Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Helm repository commands #116

Merged
merged 3 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cmd/tk/bootstrap_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ the bootstrap command will perform an upgrade if needed.`,
export GITHUB_TOKEN=<my-token>

# Run bootstrap for a private repo owned by a GitHub organization
bootstrap github --owner=<organization> --repository=<repo name>
tk bootstrap github --owner=<organization> --repository=<repo name>

# Run bootstrap for a private repo and assign organization teams to it
bootstrap github --owner=<organization> --repository=<repo name> --team=<team1 slug> --team=<team2 slug>
tk bootstrap github --owner=<organization> --repository=<repo name> --team=<team1 slug> --team=<team2 slug>

# Run bootstrap for a repository path
bootstrap github --owner=<organization> --repository=<repo name> --path=dev-cluster
tk bootstrap github --owner=<organization> --repository=<repo name> --path=dev-cluster

# Run bootstrap for a public repository on a personal account
bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true
tk bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true

# Run bootstrap for a private repo hosted on GitHub Enterprise
bootstrap github --owner=<organization> --repository=<repo name> --hostname=<domain>
tk bootstrap github --owner=<organization> --repository=<repo name> --hostname=<domain>
`,
RunE: bootstrapGitHubCmdRun,
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/tk/bootstrap_gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,16 @@ the bootstrap command will perform an upgrade if needed.`,
export GITLAB_TOKEN=<my-token>

# Run bootstrap for a private repo owned by a GitLab group
bootstrap gitlab --owner=<group> --repository=<repo name>
tk bootstrap gitlab --owner=<group> --repository=<repo name>

# Run bootstrap for a repository path
bootstrap gitlab --owner=<group> --repository=<repo name> --path=dev-cluster
tk bootstrap gitlab --owner=<group> --repository=<repo name> --path=dev-cluster

# Run bootstrap for a public repository on a personal account
bootstrap gitlab --owner=<user> --repository=<repo name> --private=false --personal=true
tk bootstrap gitlab --owner=<user> --repository=<repo name> --private=false --personal=true

# Run bootstrap for a private repo hosted on a GitLab server
bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain>
tk bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain>
`,
RunE: bootstrapGitLabCmdRun,
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/tk/create_kustomization.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ var createKsCmd = &cobra.Command{
Short: "Create or update a Kustomization resource",
Long: "The kustomization source create command generates a Kustomize resource for a given GitRepository source.",
Example: ` # Create a Kustomization resource from a source at a given path
create kustomization contour \
tk create kustomization contour \
--source=contour \
--path="./examples/contour/" \
--prune=true \
Expand All @@ -51,7 +51,7 @@ var createKsCmd = &cobra.Command{
--health-check-timeout=3m

# Create a Kustomization resource that depends on the previous one
create kustomization webapp \
tk create kustomization webapp \
--depends-on=contour \
--source=webapp \
--path="./deploy/overlays/dev" \
Expand All @@ -60,7 +60,7 @@ var createKsCmd = &cobra.Command{
--validation=client

# Create a Kustomization resource that runs under a service account
create kustomization webapp \
tk create kustomization webapp \
--source=webapp \
--path="./deploy/overlays/staging" \
--prune=true \
Expand Down
14 changes: 7 additions & 7 deletions cmd/tk/create_source_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,35 +46,35 @@ The create source git command generates a GitRepository resource and waits for i
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 git podinfo \
tk create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
--branch=master

# Create a source from a Git repository pinned to specific git tag
create source git podinfo \
tk create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
--tag="3.2.3"

# Create a source from a public Git repository tag that matches a semver range
create source git podinfo \
tk create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
--tag-semver=">=3.2.0 <3.3.0"

# Create a source from a Git repository using SSH authentication
create source git podinfo \
tk create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master

# Create a source from a Git repository using SSH authentication and an
# ECDSA P-521 curve public key
create source git podinfo \
tk create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master \
--ssh-key-algorithm=ecdsa \
--ssh-ecdsa-curve=p521

# Create a source from a Git repository using basic authentication
create source git podinfo \
tk create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
--username=username \
--password=password
Expand Down Expand Up @@ -115,7 +115,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]

if sourceGitURL == "" {
return fmt.Errorf("git-url is required")
return fmt.Errorf("url is required")
}

tmpDir, err := ioutil.TempDir("", name)
Expand Down
229 changes: 229 additions & 0 deletions cmd/tk/create_source_helm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
Copyright 2020 The Flux CD contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"fmt"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
"github.com/spf13/cobra"
"io/ioutil"
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"
"net/url"
"os"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
)

var createSourceHelmCmd = &cobra.Command{
Use: "helm [name]",
Short: "Create or update a HelmRepository source",
Long: `
The create source helm command generates a HelmRepository resource and waits for it to fetch the index.
For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`,
Example: ` # Create a source from a public Helm repository
tk create source helm podinfo \
--url=https://stefanprodan.github.io/podinfo \
--interval=10m

# Create a source from a Helm repository using basic authentication
tk create source helm podinfo \
--url=https://stefanprodan.github.io/podinfo \
--username=username \
--password=password
`,
RunE: createSourceHelmCmdRun,
}

var (
sourceHelmURL string
sourceHelmUsername string
sourceHelmPassword string
)

func init() {
createSourceHelmCmd.Flags().StringVar(&sourceHelmURL, "url", "", "Helm repository address")
createSourceHelmCmd.Flags().StringVarP(&sourceHelmUsername, "username", "u", "", "basic authentication username")
createSourceHelmCmd.Flags().StringVarP(&sourceHelmPassword, "password", "p", "", "basic authentication password")

createSourceCmd.AddCommand(createSourceHelmCmd)
}

func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("source name is required")
}
name := args[0]
secretName := fmt.Sprintf("helm-%s", name)

if sourceHelmURL == "" {
return fmt.Errorf("url is required")
}

tmpDir, err := ioutil.TempDir("", name)
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)

if _, err := url.Parse(sourceHelmURL); err != nil {
return fmt.Errorf("url parse failed: %w", err)
}

ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
}

helmRepository := sourcev1.HelmRepository{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: sourcev1.HelmRepositorySpec{
URL: sourceHelmURL,
Interval: metav1.Duration{
Duration: interval,
},
},
}

if export {
return exportHelmRepository(helmRepository)
}

withAuth := false
if sourceHelmUsername != "" && sourceHelmPassword != "" {
logger.Actionf("applying secret with basic auth credentials")
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
StringData: map[string]string{
"username": sourceHelmUsername,
"password": sourceHelmPassword,
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err
}
withAuth = true
}

if withAuth {
logger.Successf("authentication configured")
}

logger.Generatef("generating source")

if withAuth {
helmRepository.Spec.SecretRef = &corev1.LocalObjectReference{
Name: secretName,
}
}

logger.Actionf("applying source")
if err := upsertHelmRepository(ctx, kubeClient, helmRepository); err != nil {
return err
}

logger.Waitingf("waiting for index download")
if err := wait.PollImmediate(pollInterval, timeout,
isHelmRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}

logger.Successf("index download completed")

namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err = kubeClient.Get(ctx, namespacedName, &helmRepository)
if err != nil {
return fmt.Errorf("helm index failed: %w", err)
}

if helmRepository.Status.Artifact != nil {
logger.Successf("fetched revision: %s", helmRepository.Status.Artifact.Revision)
} else {
return fmt.Errorf("index download failed, artifact not found")
}

return nil
}

func upsertHelmRepository(ctx context.Context, kubeClient client.Client, helmRepository sourcev1.HelmRepository) error {
namespacedName := types.NamespacedName{
Namespace: helmRepository.GetNamespace(),
Name: helmRepository.GetName(),
}

var existing sourcev1.HelmRepository
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, &helmRepository); err != nil {
return err
} else {
logger.Successf("source created")
return nil
}
}
return err
}

existing.Spec = helmRepository.Spec
if err := kubeClient.Update(ctx, &existing); err != nil {
return err
}

logger.Successf("source updated")
return nil
}

func exportHelmRepository(source sourcev1.HelmRepository) error {
gvk := sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)
export := sourcev1.HelmRepository{
TypeMeta: metav1.TypeMeta{
Kind: gvk.Kind,
APIVersion: gvk.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: source.Name,
Namespace: source.Namespace,
},
Spec: source.Spec,
}

data, err := yaml.Marshal(export)
if err != nil {
return err
}

fmt.Println("---")
fmt.Println(string(data))
return nil
}
5 changes: 4 additions & 1 deletion cmd/tk/delete_kustomization.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ var deleteKsCmd = &cobra.Command{
Aliases: []string{"ks"},
Short: "Delete a Kustomization resource",
Long: "The delete kustomization command deletes the given Kustomization from the cluster.",
RunE: deleteKsCmdRun,
Example: ` # Delete a kustomization and the Kubernetes resources created by it
tk delete kustomization podinfo
`,
RunE: deleteKsCmdRun,
}

func init() {
Expand Down
5 changes: 4 additions & 1 deletion cmd/tk/delete_source_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ var deleteSourceGitCmd = &cobra.Command{
Use: "git [name]",
Short: "Delete a GitRepository source",
Long: "The delete source git command deletes the given GitRepository from the cluster.",
RunE: deleteSourceGitCmdRun,
Example: ` # Delete a Git repository
tk delete source git podinfo
`,
RunE: deleteSourceGitCmdRun,
}

func init() {
Expand Down
Loading