Merge pull request #8 from fluxcd/create-kustomization

Implement create kustomization command
pull/9/head
Stefan Prodan 5 years ago committed by GitHub
commit 1d7b8cd54f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,240 @@
package main
import (
"context"
"fmt"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
"strings"
"time"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var createKsCmd = &cobra.Command{
Use: "kustomization [name]",
Short: "Create or update a kustomization resource",
Long: `
The kustomization source command generates a kustomization.kustomize.fluxcd.io resource for a given GitRepository source.
API spec: https://github.com/fluxcd/kustomize-controller/tree/master/docs/spec/v1alpha1`,
Example: ` # Create a kustomization from a source at a given path
create kustomization backend \
--source=webapp \
--path="./overlays/backend/" \
--prune="app=backend" \
--interval=10m \
--validate=client \
--health-check="StatefulSet/backend.test" \
--health-check-timeout=3m
# Create a kustomization that depends on another
create kustomization frontend \
--depends-on=backend \
--source=webapp \
--path="./overlays/frontend/" \
--prune="app=frontend" \
--interval=5m \
--validate=client \
--health-check="Deployment/frontend.test" \
--health-check-timeout=2m
`,
RunE: createKsCmdRun,
}
var (
ksSource string
ksPath string
ksPrune string
ksDependsOn []string
ksValidate string
ksHealthCheck []string
ksHealthTimeout time.Duration
ksGenerate bool
)
func init() {
createKsCmd.Flags().StringVar(&ksSource, "source", "", "GitRepository name")
createKsCmd.Flags().StringVar(&ksPath, "path", "./", "path to the directory containing the kustomization file")
createKsCmd.Flags().StringVar(&ksPrune, "prune", "", "label selector used for garbage collection")
createKsCmd.Flags().StringArrayVar(&ksHealthCheck, "health-check", nil, "workload to be included in the health assessment, in the format '<kind>/<name>.<namespace>'")
createKsCmd.Flags().DurationVar(&ksHealthTimeout, "health-check-timeout", 2*time.Minute, "timeout of health checking operations")
createKsCmd.Flags().StringVar(&ksValidate, "validate", "", "validate the manifests before applying them on the cluster, can be 'client' or 'server'")
createKsCmd.Flags().BoolVar(&ksGenerate, "generate", false, "generate the kustomization.yaml for all the Kubernetes manifests in the specified path and sub-directories")
createKsCmd.Flags().StringArrayVar(&ksDependsOn, "depends-on", nil, "kustomization that must be ready before this kustomization can be applied")
createCmd.AddCommand(createKsCmd)
}
func createKsCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("kustomization name is required")
}
name := args[0]
if ksSource == "" {
return fmt.Errorf("source is required")
}
if ksPath == "" {
return fmt.Errorf("path is required")
}
if !strings.HasPrefix(ksPath, "./") {
return fmt.Errorf("path must begin with ./")
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
}
logAction("generating %s kustomization", name)
emptyAPIGroup := ""
kustomization := kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: kustomizev1.KustomizationSpec{
DependsOn: ksDependsOn,
Generate: ksGenerate,
Interval: metav1.Duration{
Duration: interval,
},
Path: ksPath,
Prune: ksPrune,
SourceRef: corev1.TypedLocalObjectReference{
APIGroup: &emptyAPIGroup,
Kind: "GitRepository",
Name: ksSource,
},
Suspend: false,
Validation: ksValidate,
},
}
if len(ksHealthCheck) > 0 {
healthChecks := make([]kustomizev1.WorkloadReference, 0)
for _, w := range ksHealthCheck {
kindObj := strings.Split(w, "/")
if len(kindObj) != 2 {
return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace' %v", w, kindObj)
}
kind := kindObj[0]
kinds := map[string]bool{
"Deployment": true,
"DaemonSet": true,
"StatefulSet": true,
}
if !kinds[kind] {
return fmt.Errorf("invalid health check kind '%s' can be Deployment, DaemonSet or StatefulSet", kind)
}
nameNs := strings.Split(kindObj[1], ".")
if len(nameNs) != 2 {
return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace'", w)
}
healthChecks = append(healthChecks, kustomizev1.WorkloadReference{
Kind: kind,
Name: nameNs[0],
Namespace: nameNs[1],
})
}
kustomization.Spec.HealthChecks = healthChecks
kustomization.Spec.Timeout = &metav1.Duration{
Duration: ksHealthTimeout,
}
}
if err := upsertKustomization(ctx, kubeClient, kustomization); err != nil {
return err
}
logAction("waiting for kustomization sync")
if err := wait.PollImmediate(2*time.Second, timeout,
isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}
logSuccess("kustomization %s is ready", name)
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err = kubeClient.Get(ctx, namespacedName, &kustomization)
if err != nil {
return fmt.Errorf("kustomization sync failed: %w", err)
}
if kustomization.Status.LastAppliedRevision != "" {
logSuccess("applied revision %s", kustomization.Status.LastAppliedRevision)
} else {
return fmt.Errorf("kustomization sync failed")
}
return nil
}
func upsertKustomization(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization) error {
namespacedName := types.NamespacedName{
Namespace: kustomization.GetNamespace(),
Name: kustomization.GetName(),
}
var existing kustomizev1.Kustomization
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, &kustomization); err != nil {
return err
} else {
logSuccess("kustomization created")
return nil
}
}
return err
}
existing.Spec = kustomization.Spec
if err := kubeClient.Update(ctx, &existing); err != nil {
return err
}
logSuccess("kustomization updated")
return nil
}
func isKustomizationReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc {
return func() (bool, error) {
var kustomization kustomizev1.Kustomization
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err := kubeClient.Get(ctx, namespacedName, &kustomization)
if err != nil {
return false, err
}
for _, condition := range kustomization.Status.Conditions {
if condition.Type == sourcev1.ReadyCondition {
if condition.Status == corev1.ConditionTrue {
return true, nil
} else if condition.Status == corev1.ConditionFalse {
return false, fmt.Errorf(condition.Message)
}
}
}
return false, nil
}
}

@ -22,10 +22,11 @@ import (
var createSourceCmd = &cobra.Command{
Use: "source [name]",
Short: "Create source resource",
Short: "Create or update a source resource",
Long: `
The create source command generates a source.fluxcd.io resource and waits for it to sync.
For Git over SSH, host and SSH keys are automatically generated.`,
For Git over SSH, host and SSH keys are automatically generated and stored in a Kubernetes secret.
For private Git repositories, the basic authentication credentials are stored in a Kubernetes secret.`,
Example: ` # Create a source from a public Git repository master branch
create source podinfo --git-url https://github.com/stefanprodan/podinfo-deploy --git-branch master
@ -131,20 +132,8 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error {
return err
}
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err = kubeClient.Get(ctx, namespacedName, &gitRepository)
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, &gitRepository); err != nil {
return err
}
} else {
if err := kubeClient.Update(ctx, &gitRepository); err != nil {
return err
}
if err := upsertGitRepository(ctx, kubeClient, gitRepository); err != nil {
return err
}
logAction("waiting for source sync")
@ -155,13 +144,17 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error {
logSuccess("source %s is ready", name)
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err = kubeClient.Get(ctx, namespacedName, &gitRepository)
if err != nil {
return fmt.Errorf("source sync failed: %w", err)
}
if gitRepository.Status.Artifact != nil {
logSuccess("revision %s", gitRepository.Status.Artifact.Revision)
logSuccess("fetched revision %s", gitRepository.Status.Artifact.Revision)
} else {
return fmt.Errorf("source sync failed, artifact not found")
}
@ -222,6 +215,35 @@ func generateSSH(ctx context.Context, name, host, tmpDir string) error {
return nil
}
func upsertGitRepository(ctx context.Context, kubeClient client.Client, gitRepository sourcev1.GitRepository) error {
namespacedName := types.NamespacedName{
Namespace: gitRepository.GetNamespace(),
Name: gitRepository.GetName(),
}
var existing sourcev1.GitRepository
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, &gitRepository); err != nil {
return err
} else {
logSuccess("source created")
return nil
}
}
return err
}
existing.Spec = gitRepository.Spec
if err := kubeClient.Update(ctx, &existing); err != nil {
return err
}
logSuccess("source updated")
return nil
}
func isGitRepositoryReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc {
return func() (bool, error) {
var gitRepository sourcev1.GitRepository

Loading…
Cancel
Save