-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement kaniko builder (#227)
* feat: implement kaniko builder * fix: applied the requested changes/suggestions * fix: a typo in number 5 * fix: extract constants
- Loading branch information
1 parent
4e51835
commit ababbf2
Showing
8 changed files
with
523 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package builder | ||
|
||
import "context" | ||
|
||
type Builder interface { | ||
Build(ctx context.Context, b *BuilderOptions) (logs string, err error) | ||
} | ||
|
||
type CacheOptions struct { | ||
Enabled bool | ||
Dir string | ||
Repo string | ||
} | ||
|
||
type BuilderOptions struct { | ||
ImageName string | ||
BuildContext string | ||
Args []string | ||
Destination string | ||
Cache *CacheOptions | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package kaniko | ||
|
||
import ( | ||
"fmt" | ||
) | ||
|
||
type Error struct { | ||
Code string | ||
Message string | ||
Err error | ||
} | ||
|
||
func (e *Error) Error() string { | ||
if e.Err != nil { | ||
return fmt.Sprintf("%s: %v", e.Message, e.Err) | ||
} | ||
return e.Message | ||
} | ||
|
||
func (e *Error) Wrap(err error) error { | ||
e.Err = err | ||
return e | ||
} | ||
|
||
var ( | ||
ErrBuildContextEmpty = &Error{Code: "BuildContextEmpty", Message: "build context cannot be empty"} | ||
ErrCleaningUp = &Error{Code: "CleaningUp", Message: "error cleaning up"} | ||
ErrCreatingJob = &Error{Code: "CreatingJob", Message: "error creating Job"} | ||
ErrDeletingJob = &Error{Code: "DeletingJob", Message: "error deleting Job"} | ||
ErrDeletingPods = &Error{Code: "DeletingPods", Message: "error deleting Pods"} | ||
ErrGeneratingUUID = &Error{Code: "GeneratingUUID", Message: "error generating UUID"} | ||
ErrGettingContainerLogs = &Error{Code: "GettingContainerLogs", Message: "error getting container logs"} | ||
ErrGettingPodFromJob = &Error{Code: "GettingPodFromJob", Message: "error getting Pod from Job"} | ||
ErrListingJobs = &Error{Code: "ListingJobs", Message: "error listing Jobs"} | ||
ErrListingPods = &Error{Code: "ListingPods", Message: "error listing Pods"} | ||
ErrNoContainersFound = &Error{Code: "NoContainersFound", Message: "no containers found"} | ||
ErrNoPodsFound = &Error{Code: "NoPodsFound", Message: "no Pods found"} | ||
ErrPreparingJob = &Error{Code: "PreparingJob", Message: "error preparing Job"} | ||
ErrWaitingJobCompletion = &Error{Code: "WaitingJobCompletion", Message: "error waiting for Job completion"} | ||
ErrWatchingChannelCloseUnexpectedly = &Error{Code: "WatchingChannelCloseUnexpectedly", Message: "watch channel closed unexpectedly"} | ||
ErrWatchingJob = &Error{Code: "WatchingJob", Message: "error watching Job"} | ||
ErrContextCancelled = &Error{Code: "ContextCancelled", Message: "context cancelled"} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package kaniko | ||
|
||
import ( | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
const ( | ||
regexpGitRepoProtocol = `^(https?|git|ssh|ftp)://` | ||
regexpGitRepoDotGit = `\.git$` | ||
gitProtocol = "git://" | ||
) | ||
|
||
type GitContext struct { | ||
Repo string | ||
Commit string | ||
Username string | ||
Password string | ||
} | ||
|
||
func (g *GitContext) BuildContext() (string, error) { | ||
bCtx := "" | ||
|
||
// cleaning the repo url | ||
rgx, err := regexp.Compile(regexpGitRepoProtocol) | ||
if err != nil { | ||
return "", err | ||
} | ||
g.Repo = rgx.ReplaceAllString(g.Repo, "") | ||
|
||
rgx, err = regexp.Compile(regexpGitRepoDotGit) | ||
if err != nil { | ||
return "", err | ||
} | ||
g.Repo = rgx.ReplaceAllString(g.Repo, "") | ||
g.Repo = strings.TrimSuffix(g.Repo, "/") | ||
|
||
bCtx += gitProtocol | ||
if g.Username != "" { | ||
bCtx += g.Username | ||
if g.Password != "" { | ||
bCtx += ":" + g.Password | ||
} | ||
bCtx += "@" | ||
} | ||
|
||
bCtx += g.Repo | ||
if g.Commit != "" { | ||
bCtx += "#" + g.Commit | ||
} | ||
|
||
return bCtx, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
package kaniko | ||
|
||
import ( | ||
"context" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"fmt" | ||
|
||
"github.com/celestiaorg/knuu/pkg/builder" | ||
"github.com/celestiaorg/knuu/pkg/names" | ||
batchv1 "k8s.io/api/batch/v1" | ||
v1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
) | ||
|
||
const ( | ||
kanikoImage = "gcr.io/kaniko-project/executor:debug" // debug has a shell | ||
kanikoContainerName = "kaniko-container" | ||
kanikoJobNamePrefix = "kaniko-build-job" | ||
|
||
DefaultParallelism = int32(5) | ||
DefaultBackoffLimit = int32(5) | ||
) | ||
|
||
type Kaniko struct { | ||
K8sClientset kubernetes.Interface | ||
K8sNamespace string | ||
} | ||
|
||
var _ builder.Builder = &Kaniko{} | ||
|
||
func (k *Kaniko) Build(ctx context.Context, b *builder.BuilderOptions) (logs string, err error) { | ||
job, err := prepareJob(b) | ||
if err != nil { | ||
return "", ErrPreparingJob.Wrap(err) | ||
} | ||
|
||
cJob, err := k.K8sClientset.BatchV1().Jobs(k.K8sNamespace).Create(ctx, job, metav1.CreateOptions{}) | ||
if err != nil { | ||
return "", ErrCreatingJob.Wrap(err) | ||
} | ||
|
||
kJob, err := k.waitForJobCompletion(ctx, cJob) | ||
if err != nil { | ||
return "", ErrWaitingJobCompletion.Wrap(err) | ||
} | ||
|
||
pod, err := k.firstPodFromJob(ctx, kJob) | ||
if err != nil { | ||
return "", ErrGettingPodFromJob.Wrap(err) | ||
} | ||
|
||
logs, err = k.containerLogs(ctx, pod) | ||
if err != nil { | ||
return "", ErrGettingContainerLogs.Wrap(err) | ||
} | ||
|
||
if err := k.cleanup(ctx, kJob); err != nil { | ||
return "", ErrCleaningUp.Wrap(err) | ||
} | ||
|
||
return logs, nil | ||
} | ||
|
||
func (k *Kaniko) waitForJobCompletion(ctx context.Context, job *batchv1.Job) (*batchv1.Job, error) { | ||
watcher, err := k.K8sClientset.BatchV1().Jobs(k.K8sNamespace).Watch(ctx, metav1.ListOptions{ | ||
FieldSelector: fmt.Sprintf("metadata.name=%s", job.Name), | ||
}) | ||
if err != nil { | ||
return nil, ErrWatchingJob.Wrap(err) | ||
} | ||
defer watcher.Stop() | ||
|
||
for { | ||
select { | ||
case event, ok := <-watcher.ResultChan(): | ||
if !ok { | ||
return nil, ErrWatchingChannelCloseUnexpectedly | ||
} | ||
|
||
j, ok := event.Object.(*batchv1.Job) | ||
if !ok { | ||
continue | ||
} | ||
|
||
if j.Status.Succeeded > 0 || j.Status.Failed > 0 { | ||
// Job completed (successfully or failed) | ||
return j, nil | ||
} | ||
case <-ctx.Done(): | ||
return nil, ErrContextCancelled | ||
} | ||
} | ||
} | ||
|
||
func (k *Kaniko) firstPodFromJob(ctx context.Context, job *batchv1.Job) (*v1.Pod, error) { | ||
podList, err := k.K8sClientset.CoreV1().Pods(k.K8sNamespace).List(ctx, metav1.ListOptions{ | ||
LabelSelector: fmt.Sprintf("job-name=%s", job.Name), | ||
}) | ||
if err != nil { | ||
return nil, ErrListingPods.Wrap(err) | ||
} | ||
|
||
if len(podList.Items) == 0 { | ||
return nil, ErrNoPodsFound.Wrap(fmt.Errorf("job: %s", job.Name)) | ||
} | ||
|
||
return &podList.Items[0], nil | ||
} | ||
|
||
func (k *Kaniko) containerLogs(ctx context.Context, pod *v1.Pod) (string, error) { | ||
if len(pod.Spec.Containers) == 0 { | ||
return "", ErrNoContainersFound.Wrap(fmt.Errorf("pod: %s", pod.Name)) | ||
} | ||
|
||
containerName := pod.Spec.Containers[0].Name | ||
|
||
logOptions := v1.PodLogOptions{ | ||
Container: containerName, | ||
} | ||
|
||
req := k.K8sClientset.CoreV1().Pods(k.K8sNamespace).GetLogs(pod.Name, &logOptions) | ||
logs, err := req.DoRaw(ctx) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return string(logs), nil | ||
} | ||
|
||
func (k *Kaniko) cleanup(ctx context.Context, job *batchv1.Job) error { | ||
err := k.K8sClientset.BatchV1().Jobs(k.K8sNamespace). | ||
Delete(ctx, job.Name, metav1.DeleteOptions{ | ||
PropagationPolicy: &[]metav1.DeletionPropagation{metav1.DeletePropagationBackground}[0], | ||
}) | ||
if err != nil { | ||
return ErrDeletingJob.Wrap(err) | ||
} | ||
|
||
// Delete the associated Pods | ||
err = k.K8sClientset.CoreV1().Pods(k.K8sNamespace). | ||
DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ | ||
LabelSelector: fmt.Sprintf("job-name=%s", job.Name), | ||
}) | ||
if err != nil { | ||
return ErrDeletingPods.Wrap(err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func DefaultCacheOptions(buildContext string) (*builder.CacheOptions, error) { | ||
if buildContext == "" { | ||
return nil, ErrBuildContextEmpty | ||
} | ||
hash := sha256.New() | ||
_, err := hash.Write([]byte(buildContext)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
hashStr := hex.EncodeToString(hash.Sum(nil)) | ||
|
||
return &builder.CacheOptions{ | ||
Enabled: true, | ||
Dir: "", | ||
Repo: fmt.Sprintf("ttl.sh/%s:24h", hashStr), | ||
}, nil | ||
} | ||
|
||
func prepareJob(b *builder.BuilderOptions) (*batchv1.Job, error) { | ||
jobName, err := names.NewRandomK8(kanikoJobNamePrefix) | ||
if err != nil { | ||
return nil, ErrGeneratingUUID.Wrap(err) | ||
} | ||
|
||
parallelism := DefaultParallelism | ||
backoffLimit := DefaultBackoffLimit | ||
job := &batchv1.Job{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: jobName, | ||
}, | ||
Spec: batchv1.JobSpec{ | ||
Parallelism: ¶llelism, // Set parallelism to 1 to ensure only one Pod | ||
BackoffLimit: &backoffLimit, // Retry the Job at most 5 times | ||
Template: v1.PodTemplateSpec{ | ||
Spec: v1.PodSpec{ | ||
Containers: []v1.Container{ | ||
{ | ||
Name: kanikoContainerName, | ||
Image: kanikoImage, // debug has a shell | ||
Args: []string{ | ||
`--context=` + b.BuildContext, | ||
// TODO: see if we need it or not | ||
// --git gitoptions Branch to clone if build context is a git repository (default branch=,single-branch=false,recurse-submodules=false) | ||
|
||
// TODO: we might need to add some options to get the auth token for the registry | ||
"--destination=" + b.Destination, | ||
}, | ||
}, | ||
}, | ||
RestartPolicy: "Never", // Ensure that the Pod does not restart | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
// TODO: we need to add some configs to get the auth token for the cache repo | ||
if b.Cache != nil && b.Cache.Enabled { | ||
cacheArgs := []string{"--cache=true"} | ||
if b.Cache.Dir != "" { | ||
cacheArgs = append(cacheArgs, "--cache-dir="+b.Cache.Dir) | ||
} | ||
if b.Cache.Repo != "" { | ||
cacheArgs = append(cacheArgs, "--cache-repo="+b.Cache.Repo) | ||
} | ||
job.Spec.Template.Spec.Containers[0].Args = append(job.Spec.Template.Spec.Containers[0].Args, cacheArgs...) | ||
} | ||
|
||
// Add extra args | ||
job.Spec.Template.Spec.Containers[0].Args = append(job.Spec.Template.Spec.Containers[0].Args, b.Args...) | ||
|
||
return job, nil | ||
|
||
} |
Oops, something went wrong.