Skip to content

Commit

Permalink
ISSUE-30 Migrate CLI from kingpin to cobra
Browse files Browse the repository at this point in the history
  • Loading branch information
hypnoglow committed Aug 19, 2022
1 parent 80f84c3 commit 70cdba9
Show file tree
Hide file tree
Showing 30 changed files with 927 additions and 499 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:

- name: Build
run: |
go build -v -o ./bin/helm-s3 ./cmd/helms3
go build -v -o ./bin/helm-s3 ./cmd/helm-s3
test-e2e:
name: Run end-to-end tests
Expand Down Expand Up @@ -111,9 +111,9 @@ jobs:
tmp_dir="$(mktemp -d)"
go build \
-o bin/helms3 \
-o bin/helm-s3 \
-ldflags "-X main.version=${plugin_version}" \
./cmd/helms3
./cmd/helm-s3
# Copy plugin directory to outside of the workspace.
cp -r ${{ github.workspace }} ${tmp_dir}
Expand Down
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ linters:
- gofmt
- goheader
- goimports
- gomnd
# - gomnd
- gomodguard
- goprintffuncname
- gosec
Expand Down
4 changes: 2 additions & 2 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
builds:
- main: ./cmd/helms3
binary: ./bin/helms3
- main: ./cmd/helm-s3
binary: ./bin/helm-s3
flags:
- -trimpath
env:
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ WORKDIR /workspace/helm-s3

COPY . .

RUN CGO_ENABLED=0 go build -o bin/helms3 \
RUN CGO_ENABLED=0 go build -o bin/helm-s3 \
-mod=vendor \
-ldflags "-X main.version=${PLUGIN_VERSION}" \
./cmd/helms3
./cmd/helm-s3

# Correct the plugin manifest with docker-specific fixes:
# - remove hooks, because we are building everything locally from source
Expand All @@ -37,7 +37,7 @@ LABEL org.label-schema.build-date=$BUILD_DATE \
org.label-schema.schema-version="1.0"

COPY --from=build /workspace/helm-s3/plugin.yaml.fixed /root/.helm/cache/plugins/helm-s3/plugin.yaml
COPY --from=build /workspace/helm-s3/bin/helms3 /root/.helm/cache/plugins/helm-s3/bin/helms3
COPY --from=build /workspace/helm-s3/bin/helm-s3 /root/.helm/cache/plugins/helm-s3/bin/helm-s3

RUN mkdir -p /root/.helm/plugins \
&& helm plugin install /root/.helm/cache/plugins/helm-s3
Expand Down
135 changes: 135 additions & 0 deletions cmd/helm-s3/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package main

import (
"context"
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/hypnoglow/helm-s3/internal/awss3"
"github.com/hypnoglow/helm-s3/internal/awsutil"
"github.com/hypnoglow/helm-s3/internal/helmutil"
)

const deleteDesc = `This command removes a chart from the repository.
'helm s3 init' takes two arguments:
- NAME - name of the chart to delete,
- REPO - target repository.
`

const deleteExample = ` helm s3 delete epicservice --version 0.5.1 my-repo - removes the chart with name 'epicservice' and version 0.5.1 from the repository with name 'my-repo'.`

func newDeleteCommand(opts *options) *cobra.Command {
act := &deleteAction{
printer: nil,
acl: "",
chartName: "",
repoName: "",
version: "",
}

cmd := &cobra.Command{
Use: "delete NAME REPO",
Aliases: []string{"del"},
Short: "Delete chart from the repository.",
Long: deleteDesc,
Example: deleteExample,
Args: wrapPositionalArgsBadUsage(cobra.ExactArgs(2)),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// No completions for the NAME and REPO arguments.
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
act.printer = cmd
act.acl = opts.acl
act.chartName = args[0]
act.repoName = args[1]
return act.run(cmd.Context())
},
}

flags := cmd.Flags()
flags.StringVar(&act.version, "version", act.version, "Version of the chart to delete.")
_ = cobra.MarkFlagRequired(flags, "version")

return cmd
}

type deleteAction struct {
printer printer

// global flags

acl string

// args

chartName string
repoName string

// flags

version string
}

func (act *deleteAction) run(ctx context.Context) error {
repoEntry, err := helmutil.LookupRepoEntry(act.repoName)
if err != nil {
return err
}

sess, err := awsutil.Session(awsutil.DynamicBucketRegion(repoEntry.URL()))
if err != nil {
return err
}
storage := awss3.New(sess)

// Fetch current index.
b, err := storage.FetchRaw(ctx, repoEntry.IndexURL())
if err != nil {
return errors.WithMessage(err, "fetch current repo index")
}

idx := helmutil.NewIndex()
if err := idx.UnmarshalBinary(b); err != nil {
return errors.WithMessage(err, "load index from downloaded file")
}

// Update index.

url, err := idx.Delete(act.chartName, act.version)
if err != nil {
return err
}

idxReader, err := idx.Reader()
if err != nil {
return errors.Wrap(err, "get index reader")
}

// Delete the file from S3 and replace index file.

if url != "" {
// For relative URLs we need to prepend base URL.
if !strings.HasPrefix(url, repoEntry.URL()) {
url = strings.TrimSuffix(repoEntry.URL(), "/") + "/" + url
}

if err := storage.Delete(ctx, url); err != nil {
return errors.WithMessage(err, "delete chart file from s3")
}
}

if err := storage.PutIndex(ctx, repoEntry.URL(), act.acl, idxReader); err != nil {
return errors.WithMessage(err, "upload new index to s3")
}

if err := idx.WriteFile(repoEntry.CacheFile(), helmutil.DefaultIndexFilePerm); err != nil {
return errors.WithMessage(err, "update local index")
}

act.printer.Printf("Successfully deleted the chart from the repository.\n")
return nil
}
102 changes: 102 additions & 0 deletions cmd/helm-s3/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"fmt"
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/hypnoglow/helm-s3/internal/awss3"
"github.com/hypnoglow/helm-s3/internal/awsutil"
)

const downloadDesc = `This command downloads a chart from AWS S3.
Note that this command basically implements downloader plugin for Helm
and not intended to be run explicitly. For more information, see:
https://helm.sh/docs/topics/plugins/#downloader-plugins
'helm s3 download' takes four arguments:
- CERT - certificate file,
- KEY - key file,
- CA - certificate authority file,
- URL - full url.
`

func newDownloadCommand() *cobra.Command {
act := &downloadAction{
certFile: "",
keyFile: "",
caFile: "",
url: "",
}

cmd := &cobra.Command{
Use: "download CERT KEY CA URL",
Short: "Download chart from AWS S3.",
Long: downloadDesc,
Example: "",
Args: wrapPositionalArgsBadUsage(cobra.ExactArgs(4)),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// No completions for the arguments.
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
act.printer = cmd
act.certFile = args[0]
act.keyFile = args[1]
act.caFile = args[2]
act.url = args[3]
return act.run(cmd.Context())
},
Hidden: true,
}

return cmd
}

type downloadAction struct {
printer printer

// args

certFile string
keyFile string
caFile string
url string
}

func (act *downloadAction) run(ctx context.Context) error {
const indexYaml = "index.yaml"

sess, err := awsutil.Session(
awsutil.AssumeRoleTokenProvider(awsutil.StderrTokenProvider),
awsutil.DynamicBucketRegion(act.url),
)
if err != nil {
return err
}

storage := awss3.New(sess)

b, err := storage.FetchRaw(ctx, act.url)
if err != nil {
if strings.HasSuffix(act.url, indexYaml) && err == awss3.ErrObjectNotFound {
act.printer.PrintErrf(
"The index file does not exist by the path %s. "+
"If you haven't initialized the repository yet, try running `helm s3 init %s`",
act.url,
strings.TrimSuffix(strings.TrimSuffix(act.url, indexYaml), "/"),
)
return newSilentError()
}

return errors.WithMessage(err, fmt.Sprintf("fetch from s3 url=%s", act.url))
}

// Do not use printer, use os.Stdout directly, as required by Helm.
fmt.Print(string(b))
return nil
}
85 changes: 85 additions & 0 deletions cmd/helm-s3/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"context"

"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/hypnoglow/helm-s3/internal/awss3"
"github.com/hypnoglow/helm-s3/internal/awsutil"
"github.com/hypnoglow/helm-s3/internal/helmutil"
)

const initDesc = `This command initializes an empty repository on AWS S3.
'helm s3 init' takes one argument:
- URI - URI of the repository.
`

const initExample = ` helm s3 init s3://awesome-bucket/charts - inits chart repository in 'awesome-bucket' bucket under 'charts' path.`

func newInitCommand(opts *options) *cobra.Command {
act := &initAction{
printer: nil,
acl: "",
uri: "",
}

cmd := &cobra.Command{
Use: "init URI",
Short: "Initialize empty repository on AWS S3.",
Long: initDesc,
Example: initExample,
Args: wrapPositionalArgsBadUsage(cobra.ExactArgs(1)),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// No completions for the URI argument.
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
act.printer = cmd
act.acl = opts.acl
act.uri = args[0]
return act.run(cmd.Context())
},
}

return cmd
}

type initAction struct {
printer printer

// global flags

acl string

// args

uri string
}

func (act *initAction) run(ctx context.Context) error {
r, err := helmutil.NewIndex().Reader()
if err != nil {
return errors.WithMessage(err, "get index reader")
}

sess, err := awsutil.Session(awsutil.DynamicBucketRegion(act.uri))
if err != nil {
return err
}
storage := awss3.New(sess)

if err := storage.PutIndex(ctx, act.uri, act.acl, r); err != nil {
return errors.WithMessage(err, "upload index to s3")
}

// TODO:
// do we need to automatically do `helm repo add <name> <uri>`,
// like we are doing `helm repo update` when we push a chart
// with this plugin?

act.printer.Printf("Initialized empty repository at %s\n", act.uri)
return nil
}
Loading

0 comments on commit 70cdba9

Please sign in to comment.