From 0d4ea37ea1ea57a450bfb08662e3b66360a970da Mon Sep 17 00:00:00 2001 From: Anthony Juckel Date: Thu, 20 Jun 2024 17:32:48 -0500 Subject: [PATCH 1/5] feat: add initial implementation --- .gitignore | 3 +- DOCS.md | 101 +++++++- Dockerfile | 25 ++ Makefile | 271 ++++++++++++++++++++ cmd/vela-manifest-tool/command.go | 49 ++++ cmd/vela-manifest-tool/command_test.go | 53 ++++ cmd/vela-manifest-tool/main.go | 191 ++++++++++++++ cmd/vela-manifest-tool/main_test.go | 22 ++ cmd/vela-manifest-tool/manifestspec.go | 158 ++++++++++++ cmd/vela-manifest-tool/manifestspec_test.go | 196 ++++++++++++++ cmd/vela-manifest-tool/plugin.go | 145 +++++++++++ cmd/vela-manifest-tool/plugin_test.go | 89 +++++++ cmd/vela-manifest-tool/registry.go | 98 +++++++ cmd/vela-manifest-tool/registry_test.go | 135 ++++++++++ cmd/vela-manifest-tool/repo.go | 53 ++++ cmd/vela-manifest-tool/repo_test.go | 84 ++++++ go.mod | 26 ++ go.sum | 54 ++++ version/version.go | 64 +++++ 19 files changed, 1814 insertions(+), 3 deletions(-) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/vela-manifest-tool/command.go create mode 100644 cmd/vela-manifest-tool/command_test.go create mode 100644 cmd/vela-manifest-tool/main.go create mode 100644 cmd/vela-manifest-tool/main_test.go create mode 100644 cmd/vela-manifest-tool/manifestspec.go create mode 100644 cmd/vela-manifest-tool/manifestspec_test.go create mode 100644 cmd/vela-manifest-tool/plugin.go create mode 100644 cmd/vela-manifest-tool/plugin_test.go create mode 100644 cmd/vela-manifest-tool/registry.go create mode 100644 cmd/vela-manifest-tool/registry_test.go create mode 100644 cmd/vela-manifest-tool/repo.go create mode 100644 cmd/vela-manifest-tool/repo_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 version/version.go diff --git a/.gitignore b/.gitignore index 7b57fe7..0414e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ release/ # Local testing files -.secrets.env \ No newline at end of file +.secrets.env +*~ diff --git a/DOCS.md b/DOCS.md index cedd8bf..d7af212 100644 --- a/DOCS.md +++ b/DOCS.md @@ -1,5 +1,102 @@ ## Description -TODO: FILL ME +This plugin enables you to build and publish [Docker Manifest List](https://www.docker.com/) +or [OCI Image Index](https://github.com/opencontainers/image-spec/blob/main/image-index.md) +in a Vela pipeline. + +Source Code: https://github.com/go-vela/vela-manifest-tool + +Registry: https://hub.docker.com/r/target/vela-manifest-tool + +## Usage + +> **NOTE:** +> +> Users should refrain from using latest as the tag for the Docker image. +> +> It is recommended to use a semantically versioned tag instead. + +Sample of building and publishing an image: + +```yaml +steps: + - name: publish_hello-world + image: target/vela-manifest-tool:latest + pull: always + parameters: + registry: index.docker.io + repo: /octocat/hello-world + tags: [ "latest" ] + platforms: + - linux/amd64 + - linux/arm64/v8 + component_template: /octocat/hello-world:latest-{{ .Os }}-{{ .Arch }}{{ if .Variant }}-{{ .Variant }}{{ end }} +``` + +NOTE: For vela-manifest-tool, unlike for vela-kaniko, the `repo` argument excludes the `registry` value. Said another +way, rather than using: + +```yaml +parameters: + registry: index.docker.io + repo: index.docker.io/octocat/hello-world + ... + component_template: index.docker.io/octocat/hello-world:latest-{{ .Os }}-{{ .Arch }}{{ if .Variant }}-{{ .Variant }}{{ end }} +``` + +You must instead use: + +```yaml +parameters: + registry: index.docker.io + repo: /octocat/hello-world + ... + component_template: /octocat/hello-world:latest-{{ .Os }}-{{ .Arch }}{{ if .Variant }}-{{ .Variant }}{{ end }} +``` + +This is because manifest tool requires that all image repos referenced exist within the same registry. Resulting tags will +all be the concatenation of the registry with the repo. + +Sample of building an image without publishing: + +```yaml +steps: + - name: publish_hello-world + image: target/vela-manifest-tool:latest + pull: always + parameters: ++ dry_run: true + registry: index.docker.io + repo: /octocat/hello-world + tags: [ "latest" ] + platforms: + - linux/amd64 + - linux/arm64/v8 + component_template: /octocat/hello-world:latest-{{ .Os }}-{{ .Arch }}{{ if .Variant }}-{{ .Variant }}{{ end }} +``` + +For every element of `tags:`, one spec file will be generated and (unless `dry_run: true`) pushed to the `registry:`. +For each manifest-tool spec file, the tag for the manifest list/image index will be `$registry$repo:$tag`. Then there will +be one element in the `manifests:` list of the spec file for each element of the `platform:` argument. Platform is assumed +to be in `os/architecture/variant` format. Within the `component_template`, you can use Os, Arch, Variant (from the platform), +or Tag (from the top level `tags:`). + +Note: The default component_template of `"{{.Repo}}:{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Variant}}-{{.Variant}}{{end}}"` might +be sufficient for most needs if you follow that tagging convention. For example, if the builds for /octocat/hello-world created +the architecture specific image + +- index.docker.io/octocat/hello-world:latest-linux-amd64 +- index.docker.io/octocat/hello-world:latest-linux-arm64-v8 + +Then the following configuration would be sufficient due to defaults for tags, platforms, and component_template: + +```yaml +steps: + - name: publish_hello-world + image: target/vela-manifest-tool:latest + pull: always + parameters: + registry: index.docker.io + repo: /octocat/hello-world +``` -see: https://github.com/go-vela/vela-kaniko/blob/main/DOCS.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5467076 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: Apache-2.0 + +################################################################################ +## docker build --no-cache --target certs -t vela-manifest-tool:certs . ## +################################################################################ + +FROM alpine:3.19.1@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b as certs + +RUN apk add --update --no-cache ca-certificates + +################################################################# +## docker build --no-cache -t vela-manifest-tool:local . ## +################################################################# + +FROM mplatform/manifest-tool:alpine-v2.1.6@sha256:96db9e944c50a5f7514394af4e44f764725645cfd2efef92d87095b0016a55ae + +COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +WORKDIR /workspace + +RUN mkdir /root/.docker + +COPY release/vela-manifest-tool /bin/vela-manifest-tool + +ENTRYPOINT [ "/bin/vela-manifest-tool" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d6ef1d8 --- /dev/null +++ b/Makefile @@ -0,0 +1,271 @@ +# SPDX-License-Identifier: Apache-2.0 + +# capture the current date we build the application from +BUILD_DATE = $(shell date +%Y-%m-%dT%H:%M:%SZ) + +# check if a git commit sha is already set +ifndef GITHUB_SHA + # capture the current git commit sha we build the application from + GITHUB_SHA = $(shell git rev-parse HEAD) +endif + +# check if a git tag is already set +ifndef GITHUB_TAG + # capture the current git tag we build the application from + GITHUB_TAG = $(shell git describe --tag --abbrev=0) +endif + +# check if a go version is already set +ifndef GOLANG_VERSION + # capture the current go version we build the application from + GOLANG_VERSION = $(shell go version | awk '{ print $$3 }') +endif + +# create a list of linker flags for building the golang application +LD_FLAGS = -X github.com/go-vela/vela-manifest-tool/version.Commit=${GITHUB_SHA} -X github.com/go-vela/vela-manifest-tool/version.Date=${BUILD_DATE} -X github.com/go-vela/vela-manifest-tool/version.Go=${GOLANG_VERSION} -X github.com/go-vela/vela-manifest-tool/version.Tag=${GITHUB_TAG} + +# The `clean` target is intended to clean the workspace +# and prepare the local changes for submission. +# +# Usage: `make clean` +.PHONY: clean +clean: tidy vet fmt fix + +# The `run` target is intended to build and +# execute the Docker image for the plugin. +# +# Usage: `make run` +.PHONY: run +run: build docker-build docker-run + +# The `tidy` target is intended to clean up +# the Go module files (go.mod & go.sum). +# +# Usage: `make tidy` +.PHONY: tidy +tidy: + @echo + @echo "### Tidying Go module" + @go mod tidy + +# The `vet` target is intended to inspect the +# Go source code for potential issues. +# +# Usage: `make vet` +.PHONY: vet +vet: + @echo + @echo "### Vetting Go code" + @go vet ./... + +# The `fmt` target is intended to format the +# Go source code to meet the language standards. +# +# Usage: `make fmt` +.PHONY: fmt +fmt: + @echo + @echo "### Formatting Go Code" + @go fmt ./... + +# The `fix` target is intended to rewrite the +# Go source code using old APIs. +# +# Usage: `make fix` +.PHONY: fix +fix: + @echo + @echo "### Fixing Go Code" + @go fix ./... + +# The `test` target is intended to run +# the tests for the Go source code. +# +# Usage: `make test` +.PHONY: test +test: + @echo + @echo "### Testing Go Code" + @go test -race ./... + +# The `test-cover` target is intended to run +# the tests for the Go source code and then +# open the test coverage report. +# +# Usage: `make test-cover` +.PHONY: test-cover +test-cover: + @echo + @echo "### Creating test coverage report" + @go test -race -covermode=atomic -coverprofile=coverage.out ./... + @echo + @echo "### Opening test coverage report" + @go tool cover -html=coverage.out + +# The `build` target is intended to compile +# the Go source code into a binary. +# +# Usage: `make build` +.PHONY: build +build: + @echo + @echo "### Building release/vela-manifest-tool binary" + GOOS=linux CGO_ENABLED=0 \ + go build -a \ + -ldflags '${LD_FLAGS}' \ + -o release/vela-manifest-tool \ + github.com/go-vela/vela-manifest-tool/cmd/vela-manifest-tool + +# The `build-static` target is intended to compile +# the Go source code into a statically linked binary. +# +# Usage: `make build-static` +.PHONY: build-static +build-static: + @echo + @echo "### Building static release/vela-manifest-tool binary" + GOOS=linux CGO_ENABLED=0 \ + go build -a \ + -ldflags '-s -w -extldflags "-static" ${LD_FLAGS}' \ + -o release/vela-manifest-tool \ + github.com/go-vela/vela-manifest-tool/cmd/vela-manifest-tool + +.PHONY: build-static-amd64 +build-static-amd64: + @echo + @echo "### Building static release/vela-manifest-tool binary" + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \ + go build -a \ + -ldflags '-s -w -extldflags "-static" ${LD_FLAGS}' \ + -o release/vela-manifest-tool \ + github.com/go-vela/vela-manifest-tool/cmd/vela-manifest-tool + +# The `build-static-ci` target is intended to compile +# the Go source code into a statically linked binary +# when used within a CI environment. +# +# Usage: `make build-static-ci` +.PHONY: build-static-ci +build-static-ci: + @echo + @echo "### Building CI static release/vela-manifest-tool binary" + @go build -a \ + -ldflags '-s -w -extldflags "-static" ${LD_FLAGS}' \ + -o release/vela-manifest-tool \ + github.com/go-vela/vela-manifest-tool/cmd/vela-manifest-tool + +# The `check` target is intended to output all +# dependencies from the Go module that need updates. +# +# Usage: `make check` +.PHONY: check +check: check-install + @echo + @echo "### Checking dependencies for updates" + @go list -u -m -json all | go-mod-outdated -update + +# The `check-direct` target is intended to output direct +# dependencies from the Go module that need updates. +# +# Usage: `make check-direct` +.PHONY: check-direct +check-direct: check-install + @echo + @echo "### Checking direct dependencies for updates" + @go list -u -m -json all | go-mod-outdated -direct + +# The `check-full` target is intended to output +# all dependencies from the Go module. +# +# Usage: `make check-full` +.PHONY: check-full +check-full: check-install + @echo + @echo "### Checking all dependencies for updates" + @go list -u -m -json all | go-mod-outdated + +# The `check-install` target is intended to download +# the tool used to check dependencies from the Go module. +# +# Usage: `make check-install` +.PHONY: check-install +check-install: + @echo + @echo "### Installing psampaz/go-mod-outdated" + @go get -u github.com/psampaz/go-mod-outdated + +# The `bump-deps` target is intended to upgrade +# non-test dependencies for the Go module. +# +# Usage: `make bump-deps` +.PHONY: bump-deps +bump-deps: check + @echo + @echo "### Upgrading dependencies" + @go get -u ./... + +# The `bump-deps-full` target is intended to upgrade +# all dependencies for the Go module. +# +# Usage: `make bump-deps-full` +.PHONY: bump-deps-full +bump-deps-full: check + @echo + @echo "### Upgrading all dependencies" + @go get -t -u ./... + +# The `docker-build` target is intended to build +# the Docker image for the plugin. +# +# Usage: `make docker-build` +.PHONY: docker-build +docker-build: + @echo + @echo "### Building vela-manifest-tool:local image" + @docker build --no-cache -t vela-manifest-tool:local . + +# The `docker-test` target is intended to execute +# the Docker image for the plugin with test variables. +# +# Usage: `make docker-test` +.PHONY: docker-test +docker-test: + @echo + @echo "### Testing vela-manifest-tool:local image" + @docker run --rm \ + -e PARAMETER_CONTEXT=/workspace/ \ + -e PARAMETER_DOCKERFILE=Dockerfile.example \ + -e PARAMETER_DRY_RUN=true \ + -e PARAMETER_REGISTRY=index.docker.io \ + -e PARAMETER_REPO=index.docker.io/target/vela-manifest-tool \ + -e PARAMETER_TAGS=latest \ + -e VELA_BUILD_COMMIT=123abcdefg \ + -e VELA_BUILD_EVENT=push \ + -v $(shell pwd):/workspace \ + vela-manifest-tool:local + +# The `docker-run` target is intended to execute +# the Docker image for the plugin. +# +# Usage: `make docker-run` +.PHONY: docker-run +docker-run: + @echo + @echo "### Executing vela-manifest-tool:local image" + @echo "PARAMETER_REGISTRY: ${PARAMETER_REGISTRY}" + @docker run --rm \ + -e DOCKER_USERNAME \ + -e DOCKER_PASSWORD \ + -e PARAMETER_DRY_RUN \ + -e PARAMETER_REGISTRY \ + -e PARAMETER_REPO \ + -e PARAMETER_TAGS \ + -e VELA_BUILD_AUTHOR_EMAIL \ + -e VELA_BUILD_COMMIT \ + -e VELA_BUILD_EVENT \ + -e VELA_BUILD_NUMBER \ + -e VELA_BUILD_TAG \ + -e VELA_REPO_FULL_NAME \ + -e VELA_REPO_LINK \ + -v $(shell pwd):/workspace \ + vela-manifest-tool:local diff --git a/cmd/vela-manifest-tool/command.go b/cmd/vela-manifest-tool/command.go new file mode 100644 index 0000000..d384f6b --- /dev/null +++ b/cmd/vela-manifest-tool/command.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" +) + +const manifestToolBin = "/manifest-tool" + +// private variables just for test mocking +var stdout io.Writer = os.Stdout +var stderr io.Writer = os.Stderr + +// execCmd is a helper function to +// run the provided command. +func execCmd(e *exec.Cmd) error { + logrus.Tracef("executing cmd %s", strings.Join(e.Args, " ")) + + // set command stdout to OS stdout + e.Stdout = stdout + // set command stderr to OS stderr + e.Stderr = stderr + + // output "trace" string for command + fmt.Println("$", strings.Join(e.Args, " ")) + + return e.Run() +} + +// versionCmd is a helper function to output +// the client version information. +func versionCmd() *exec.Cmd { + logrus.Trace("creating manifest-tool version command") + + // variable to store flags for command + var flags []string + + // add flag to print version of manifest-tool command + flags = append(flags, "--version") + + return exec.Command(manifestToolBin, flags...) +} diff --git a/cmd/vela-manifest-tool/command_test.go b/cmd/vela-manifest-tool/command_test.go new file mode 100644 index 0000000..79e3a04 --- /dev/null +++ b/cmd/vela-manifest-tool/command_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "bytes" + "os/exec" + "strings" + "testing" +) + +func TestVersion(t *testing.T) { + cmd := versionCmd() + cases := []struct { + arg, expected string + }{ + {cmd.Args[0], "manifest-tool"}, + {cmd.Args[1], "--version"}, + } + for _, tc := range cases { + if !strings.Contains(tc.arg, tc.expected) { + t.Errorf(`Expected %v to contain %q`, tc.arg, tc.expected) + } + } +} + +// Feels like execCmd should be written/tested in shared lib +func TestExecution(t *testing.T) { + cases := []struct { + args []string + expout, experr string + }{ + {[]string{"echo", "-n", "foo"}, "foo", ""}, + } + oldStdout := stdout + defer func() { stdout = oldStdout }() + oldStderr := stderr + defer func() { stderr = oldStderr }() + for _, tc := range cases { + var outbuf, errbuf bytes.Buffer + + stdout, stderr = &outbuf, &errbuf + cmd := exec.Command(tc.args[0], tc.args[1:]...) + err := execCmd(cmd) + if err != nil { + t.Errorf("Expected no error when creating command: %v", err) + } + if tc.expout != outbuf.String() { + t.Errorf("Expected %q to be equal to %q", outbuf.String(), tc.expout) + } + if tc.experr != errbuf.String() { + t.Errorf("Expected %q to be equal to %q", errbuf.String(), tc.experr) + } + } +} diff --git a/cmd/vela-manifest-tool/main.go b/cmd/vela-manifest-tool/main.go new file mode 100644 index 0000000..acf1fd9 --- /dev/null +++ b/cmd/vela-manifest-tool/main.go @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "github.com/go-vela/vela-manifest-tool/version" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + _ "github.com/joho/godotenv/autoload" +) + +//nolint:funlen // ignore function length due to comments and flags +func main() { + v := version.New() + + // serialize the version information as pretty JSON + bytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + logrus.Fatal(err) + } + + // output the version information to stdout + fmt.Fprintf(os.Stdout, "%s\n", string(bytes)) + + // create new CLI application + app := cli.NewApp() + + // Plugin Information + + app.Name = "vela-manifest-tool" + app.HelpName = "vela-manifest-tool" + app.Usage = "Vela Manifest Tool plugin for building and publishing manifest lists/image indices" + app.Copyright = "Copyright 2024 Target Brands, Inc. All rights reserved." + app.Authors = []*cli.Author{ + { + Name: "Vela Admins", + Email: "vela@target.com", + }, + } + + // Plugin Metadata + + app.Action = run + app.Compiled = time.Now() + app.Version = v.Semantic() + + // Plugin Flags + + app.Flags = []cli.Flag{ + + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_LOG_LEVEL", "MANIFEST_TOOL_LOG_LEVEL"}, + FilePath: "/vela/parameters/manifest_tool/log_level,/vela/secrets/manifest_tool/log_level", + Name: "log.level", + Usage: "set log level - options: (trace|debug|info|warn|error|fatal|panic)", + Value: "info", + }, + + // Registry Flags + &cli.BoolFlag{ + EnvVars: []string{"PARAMETER_DRY_RUN", "MANIFEST_TOOL_DRY_RUN"}, + FilePath: "/vela/parameters/manifest_tool/dry_run,/vela/secrets/manifest_tool/dry_run", + Name: "registry.dry_run", + Usage: "enables building images without publishing to the registry", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_REGISTRY", "MANIFEST_TOOL_REGISTRY"}, + FilePath: "/vela/parameters/manifest_tool/registry,/vela/secrets/manifest_tool/registry", + Name: "registry.name", + Usage: "Docker registry name to communicate with", + Value: "index.docker.io", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_USERNAME", "MANIFEST_TOOL_USERNAME", "DOCKER_USERNAME"}, + FilePath: "/vela/parameters/manifest_tool/username,/vela/secrets/manifest_tool/username,/vela/secrets/managed-auth/username", + Name: "registry.username", + Usage: "user name for communication with the registry", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_PASSWORD", "MANIFEST_TOOL_PASSWORD", "DOCKER_PASSWORD"}, + FilePath: "/vela/parameters/manifest_tool/password,/vela/secrets/manifest_tool/password,/vela/secrets/managed-auth/password", + Name: "registry.password", + Usage: "password for communication with the registry", + }, + &cli.IntFlag{ + EnvVars: []string{"PARAMETER_PUSH_RETRY", "MANIFEST_TOOL_PUSH_RETRY"}, + FilePath: "/vela/parameters/manifest_tool/push_retry,/vela/secrets/manifest_tool/push_retry", + Name: "registry.push_retry", + Usage: "number of retries for pushing an image to a remote destination", + }, + + // Repo Flags + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_REPO", "MANIFEST_TOOL_REPO"}, + FilePath: "/vela/parameters/manifest_tool/repo,/vela/secrets/manifest_tool/repo", + Name: "repo.name", + Usage: "repository name for the image", + }, + &cli.StringSliceFlag{ + EnvVars: []string{"PARAMETER_TAGS", "MANIFEST_TOOL_TAGS"}, + FilePath: "/vela/parameters/manifest_tool/tags,/vela/secrets/manifest_tool/tags", + Name: "repo.tags", + Usage: "repository tags of the manifest list/image index", + Value: cli.NewStringSlice("latest"), + }, + &cli.StringSliceFlag{ + EnvVars: []string{"PARAMETER_PLATFORMS", "MANIFEST_TOOL_PLATFORMS"}, + FilePath: "/vela/parameters/manifest_tool/tags,/vela/secrets/manifest_tool/platforms", + Name: "repo.platforms", + Usage: "docker platforms to include in the manifest list/image index", + Value: cli.NewStringSlice("linux/amd64", "linux/arm64/v8"), + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_COMPONENT_TEMPLATE", "MANIFEST_TOOL_COMPONENT_TEMPLATE"}, + FilePath: "/vela/parameters/manifest_tool/component_template,/vela/secrets/manifest_tool/component_template", + Name: "repo.component_template", + Usage: "template used to render each component image", + Value: "{{.Repo}}:{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Variant}}-{{.Variant}}{{end}}", + }, + } + + err = app.Run(os.Args) + if err != nil { + logrus.Fatal(err) + } +} + +// run executes the plugin based off the configuration provided. +func run(c *cli.Context) error { + // set the log level for the plugin + switch c.String("log.level") { + case "t", "trace", "Trace", "TRACE": + logrus.SetLevel(logrus.TraceLevel) + case "d", "debug", "Debug", "DEBUG": + logrus.SetLevel(logrus.DebugLevel) + case "w", "warn", "Warn", "WARN": + logrus.SetLevel(logrus.WarnLevel) + case "e", "error", "Error", "ERROR": + logrus.SetLevel(logrus.ErrorLevel) + case "f", "fatal", "Fatal", "FATAL": + logrus.SetLevel(logrus.FatalLevel) + case "p", "panic", "Panic", "PANIC": + logrus.SetLevel(logrus.PanicLevel) + case "i", "info", "Info", "INFO": + fallthrough + default: + logrus.SetLevel(logrus.InfoLevel) + } + + logrus.WithFields(logrus.Fields{ + "code": "https://github.com/go-vela/vela-manifest-tool", + "docs": "https://go-vela.github.io/docs/plugins/registry/pipeline/manifest-tool", + "registry": "https://hub.docker.com/r/target/vela-manifest-tool", + }).Info("Vela Manifest Tool Plugin") + + // create the plugin + p := &Plugin{ + // build configuration + // registry configuration + Registry: &Registry{ + DryRun: c.Bool("registry.dry_run"), + Name: c.String("registry.name"), + Username: c.String("registry.username"), + Password: c.String("registry.password"), + PushRetry: c.Int("registry.push_retry"), + }, + // repo configuration + Repo: &Repo{ + Name: c.String("repo.name"), + Tags: c.StringSlice("repo.tags"), + Platforms: c.StringSlice("repo.platforms"), + ComponentTemplate: c.String("repo.component_template"), + }, + } + + // validate the plugin + err := p.Validate() + if err != nil { + return err + } + + // execute the plugin + return p.Exec() +} diff --git a/cmd/vela-manifest-tool/main_test.go b/cmd/vela-manifest-tool/main_test.go new file mode 100644 index 0000000..05f2541 --- /dev/null +++ b/cmd/vela-manifest-tool/main_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "testing" + + "github.com/go-vela/vela-manifest-tool/version" +) + +func TestVersionCompatible(t *testing.T) { + v := version.New() + if v == nil { + t.Error("version.New should return a value") + } +} + +func TestVersionSemver(t *testing.T) { + version.Tag = "abcd" + v := version.New() + if v != nil { + t.Errorf("version.New should return nil if a non-semver Tag (%q) is provided", version.Tag) + } +} diff --git a/cmd/vela-manifest-tool/manifestspec.go b/cmd/vela-manifest-tool/manifestspec.go new file mode 100644 index 0000000..b15cede --- /dev/null +++ b/cmd/vela-manifest-tool/manifestspec.go @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "fmt" + "io" + "strings" + "text/template" + + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +var allowedPlatforms = map[string]bool{ + "linux/amd64": true, + "linux/arm64": true, + "linux/arm64/v8": true, + "linux/arm": true, + "linux/arm/v7": true, +} + +type Manifest struct { + Spec ManifestSpec + Context ComponentContext + Template *template.Template +} + +// ManifestSpec represents the structure of the manifest-tool yaml spec file +type ManifestSpec struct { + Image string // name of the image index including tag + Manifests []ManifestComponent // list of component images to include in index +} + +type ManifestPlatform struct { + Os string + Architecture string + Variant string `yaml:",omitempty"` +} + +type ManifestComponent struct { + Image string // name of the component image to be referenced by the index + Platform ManifestPlatform // The platform specification for the component image +} + +type ComponentContext struct { + Repo string + Tag string + Os string + Arch string + Variant string +} + +func NewManifestSpec(reg *Registry, repo *Repo) ([]*ManifestSpec, error) { + specs := []*ManifestSpec{} + tmpl, err := template.New("component_template").Parse(repo.ComponentTemplate) + if err != nil { + return specs, err + } + + if len(reg.Name) == 0 { + return specs, fmt.Errorf("no registry name provided") + } + if len(repo.Name) == 0 { + return specs, fmt.Errorf("no repository name provided") + } + for _, tag := range repo.Tags { + ms := ManifestSpec{ + Image: reg.Name + repo.Name + ":" + tag, + Manifests: []ManifestComponent{}, + } + for _, platform := range repo.Platforms { + platformComp := strings.Split(platform, "/") + if len(platformComp) < 2 { + return nil, fmt.Errorf("malformed platform %s", platform) + } else if len(platformComp) == 2 { + // probably a better way to do this, just not sure how + // else to make the variant below clean + platformComp = append(platformComp, "") + } + ctx := ComponentContext{ + Repo: repo.Name, + Tag: tag, + Os: platformComp[0], + Arch: platformComp[1], + Variant: platformComp[2], + } + var compImgBuf bytes.Buffer + err = tmpl.Execute(&compImgBuf, ctx) + if err != nil { + return specs, err + } + compImg := compImgBuf.String() + comp := ManifestComponent{ + Image: fmt.Sprintf("%s%s", reg.Name, compImg), + Platform: ManifestPlatform{ + Os: ctx.Os, + Architecture: ctx.Arch, + Variant: ctx.Variant, + }, + } + ms.Manifests = append(ms.Manifests, comp) + } + specs = append(specs, &ms) + } + return specs, nil +} + +func (ms *ManifestSpec) Validate() error { + logrus.Trace("validating manifest spec plugin configuration") + + // verify repo is provided + if len(ms.Image) == 0 { + return fmt.Errorf("no top-level image provided") + } + + err := validateTagOfImage(ms.Image) + if err != nil { + return err + } + + // check if tags are provided + if len(ms.Manifests) > 0 { + // check each tag value for valid docker tag syntax + for _, compManifest := range ms.Manifests { + err = validateTagOfImage(compManifest.Image) + if err != nil { + return err + } + } + } else { + return fmt.Errorf("no component images provided") + } + + return nil +} + +func (ms *ManifestSpec) Render(wr io.Writer) error { + yamlData, err := yaml.Marshal(ms) + if err != nil { + return err + } + _, err = wr.Write(yamlData) + return err +} + +func validateTagOfImage(fullImage string) error { + topLevelImgParts := strings.Split(fullImage, ":") + + if len(topLevelImgParts) != 2 { + return fmt.Errorf("%s not in image:tag format", fullImage) + } + if !tagRegexp.MatchString(topLevelImgParts[1]) { + return fmt.Errorf(errTagValidation, topLevelImgParts[1]) + } + return nil +} diff --git a/cmd/vela-manifest-tool/manifestspec_test.go b/cmd/vela-manifest-tool/manifestspec_test.go new file mode 100644 index 0000000..1fde173 --- /dev/null +++ b/cmd/vela-manifest-tool/manifestspec_test.go @@ -0,0 +1,196 @@ +package main + +import ( + "bytes" + "testing" + + "github.com/sirupsen/logrus" +) + +func init() { + logrus.SetFormatter(&logrus.TextFormatter{}) +} + +func TestManifestSpec_New_Validate(t *testing.T) { + man := defaultFixture(t) + + assertImageMatch(t, "index.docker.io/octocat/hello-world:latest", man.Image) + assertImageMatch(t, "index.docker.io/octocat/hello-world:latest-linux-amd64", + man.Manifests[0].Image) + assertImageMatch(t, "index.docker.io/octocat/hello-world:latest-linux-arm64-v8", + man.Manifests[1].Image) + var data bytes.Buffer + err := man.Render(&data) + if err != nil { + t.Errorf("Error encountered during render: %v", err) + } + expected := "image: index.docker.io/octocat/hello-world:latest\n" + + "manifests:\n" + + "- image: index.docker.io/octocat/hello-world:latest-linux-amd64\n" + + " platform:\n" + + " os: linux\n" + + " architecture: amd64\n" + + "- image: index.docker.io/octocat/hello-world:latest-linux-arm64-v8\n" + + " platform:\n" + + " os: linux\n" + + " architecture: arm64\n" + + " variant: v8\n" + if data.String() != expected { + t.Errorf("failed yaml rendering.\nexpected:\n%sactual:\n%s", expected, data.String()) + } +} + +func TestManifestSpec_Validations(t *testing.T) { + testCases := []struct { + name string + valid bool + avail bool + ms *ManifestSpec + }{ + { + name: "missing image", + valid: false, + avail: true, + ms: trMS(t, func(ms *ManifestSpec) *ManifestSpec { ms.Image = ""; return ms }), + }, + { + name: "missing repo", + valid: false, + avail: true, + ms: trMS(t, func(ms *ManifestSpec) *ManifestSpec { ms.Image = ""; return ms }), + }, + { + name: "invalid image", + valid: false, + avail: false, + ms: firstMS(NewManifestSpec(trReg(func(r *Registry) *Registry { + r.Name = "" + return r + }), defaultRepo())), + }, + { + name: "invalid reg", + valid: false, + avail: false, + ms: firstMS(NewManifestSpec(defaultRegistry(), trRep(func(r *Repo) *Repo { + r.Name = "" + return r + }))), + }, + { + name: "invalid platform", + valid: false, + avail: false, + ms: firstMS(NewManifestSpec(defaultRegistry(), trRep(func(r *Repo) *Repo { + r.Platforms = []string{"linux"} + return r + }))), + }, + { + name: "no platforms", + valid: false, + avail: true, + ms: firstMS(NewManifestSpec(defaultRegistry(), trRep(func(r *Repo) *Repo { + r.Platforms = []string{} + return r + }))), + }, + { + name: "no tags", + valid: false, + avail: false, + ms: firstMS(NewManifestSpec(defaultRegistry(), trRep(func(r *Repo) *Repo { + r.Tags = []string{} + return r + }))), + }, + { + name: "incomplete template", + valid: false, + avail: true, + ms: firstMS(NewManifestSpec(defaultRegistry(), trRep(func(r *Repo) *Repo { + r.ComponentTemplate = "{{.Repo}}" + return r + }))), + }, + { + name: "invalid tag", + valid: false, + avail: true, + ms: firstMS(NewManifestSpec(defaultRegistry(), trRep(func(r *Repo) *Repo { + r.Tags = []string{"invalid|tag"} + return r + }))), + }, + } + for _, tc := range testCases { + if tc.avail && tc.ms == nil { + t.Errorf("%s: expected manifest spec to be available, but none returned", tc.name) + } else if !tc.avail && tc.ms != nil { + t.Errorf("%s: expected no manifest specs, but at least one returned", tc.name) + } else if tc.avail && tc.ms != nil { + err := tc.ms.Validate() + if err != nil && tc.valid { + t.Errorf("%s: expected valid ManifestSpec, but got %v", tc.name, err) + } else if err == nil && !tc.valid { + t.Errorf("%s: expected invalid ManifestSpec, but got nil", tc.name) + } + } + } +} + +func firstMS(ms []*ManifestSpec, _ error) *ManifestSpec { + if len(ms) > 0 { + return ms[0] + } + return nil +} + +func defaultRegistry() *Registry { + return &Registry{ + Name: "index.docker.io", + Username: "test", + Password: "pass", + PushRetry: 1, + DryRun: true, + } +} + +func defaultRepo() *Repo { + return &Repo{ + Name: "/octocat/hello-world", + Tags: []string{"latest"}, + Platforms: []string{"linux/amd64", "linux/arm64/v8"}, + ComponentTemplate: "{{.Repo}}:{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Variant}}-{{.Variant}}{{end}}", + } +} + +func defaultFixture(t *testing.T) *ManifestSpec { + ms, err := NewManifestSpec(defaultRegistry(), defaultRepo()) + if err != nil { + t.Fatalf("error encountered: %v", err) + } + if len(ms) != 1 { + t.Fatalf("should only have returned a single manifest spec") + } + return ms[0] +} + +// Translate ManifestSpec +func trMS(t *testing.T, f func(*ManifestSpec) *ManifestSpec) *ManifestSpec { + return f(defaultFixture(t)) +} + +func trReg(f func(r *Registry) *Registry) *Registry { + return f(defaultRegistry()) +} + +func trRep(f func(r *Repo) *Repo) *Repo { + return f(defaultRepo()) +} + +func assertImageMatch(t *testing.T, expected, actual string) { + if expected != actual { + t.Errorf("image mismatch\nexpected: %s !=\nactual: %s", expected, actual) + } +} diff --git a/cmd/vela-manifest-tool/plugin.go b/cmd/vela-manifest-tool/plugin.go new file mode 100644 index 0000000..d28cbcb --- /dev/null +++ b/cmd/vela-manifest-tool/plugin.go @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "errors" + "fmt" + "os/exec" + "regexp" + + "github.com/spf13/afero" + + "github.com/sirupsen/logrus" +) + +var ( + appFS = afero.NewOsFs() + + // regular expression to validate docker tags + // refs: + // - https://docs.docker.com/engine/reference/commandline/tag/#extended-description + // - https://github.com/distribution/distribution/blob/01f589cf8726565aa3c5c053be12873bafedbedc/reference/regexp.go#L41 + tagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) +) + +// errTagValidation defines the error message +// when the provided tag is not allowed. +const errTagValidation = "tag '%s' not allowed - see https://docs.docker.com/engine/reference/commandline/tag/#extended-description" + +// Plugin represents the configuration loaded for the plugin. +type Plugin struct { + Registry *Registry // registry arguments loaded for the plugin + Repo *Repo // repo arguments loaded for the plugin + manifestSpecs []*ManifestSpec // Parsed specs, populated as side effect of validate +} + +// Command formats and outputs the command necessary for +// manifest-tool to build and publish a Docker Manifest List or +// OCI Image Index +func (p *Plugin) Command(specFile string) *exec.Cmd { + logrus.Debug("creating manifest-tool command from plugin configuration") + + // variable to store flags for command + flags := []string{ + "push", + "from-spec", + specFile, + } + + return exec.Command(manifestToolBin, flags...) +} + +// Exec formats and runs the commands for building and publishing a Docker image. +func (p *Plugin) Exec() error { + logrus.Debug("running plugin with provided configuration") + + if len(p.manifestSpecs) == 0 { + return errors.New("no manifest specs") + } + + // create registry file for authentication + err := p.Registry.Write() + if err != nil { + return err + } + + // output the manifest-tool version for troubleshooting + err = execCmd(versionCmd()) + if err != nil { + return err + } + + manifestSpecs, err := NewManifestSpec(p.Registry, p.Repo) + if err != nil { + return err + } + a := &afero.Afero{ + Fs: appFS, + } + err = a.Mkdir("/root/specs", 0755) + if err != nil { + return err + } + + for i, spec := range manifestSpecs { + fmt.Printf("Processing manifest list/image index %s\n", spec.Image) + var data bytes.Buffer + err = spec.Render(&data) + if err != nil { + return err + } + + fmt.Printf("Rendered spec file:\n%s\n", data.String()) + specFilename := fmt.Sprintf("/root/specs/spec_%d.yml", i) + a.WriteFile(specFilename, data.Bytes(), 0644) + cmd := p.Command(specFilename) + // If a dry run, return without executing the cmd + if p.Registry.DryRun { + fmt.Println("Not pushing manifest list/image index as dry_run is true") + } else { + // run manifest-tool command from plugin configuration + err = execCmd(cmd) + if err != nil { + return err + } + } + } + + return nil +} + +// Validate verifies the Plugin is properly configured. +func (p *Plugin) Validate() error { + logrus.Debug("validating plugin configuration") + + var err error + + // validate registry configuration + err = p.Registry.Validate() + if err != nil { + return err + } + + // validate repo configuration + err = p.Repo.Validate() + if err != nil { + return err + } + + manifestSpecs, err := NewManifestSpec(p.Registry, p.Repo) + for _, ms := range manifestSpecs { + err = ms.Validate() + if err != nil { + return err + } + } + + if err != nil { + return err + } + p.manifestSpecs = manifestSpecs + + return nil +} diff --git a/cmd/vela-manifest-tool/plugin_test.go b/cmd/vela-manifest-tool/plugin_test.go new file mode 100644 index 0000000..e2116b8 --- /dev/null +++ b/cmd/vela-manifest-tool/plugin_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "testing" +) + +func TestPluginScenarios(t *testing.T) { + testCases := []struct { + name string + valid bool + p *Plugin + }{ + { + name: "all populated", + valid: true, + p: makeDefaultPlugin(), + }, + { + name: "invalid template", + valid: false, + p: trP(func(p *Plugin) *Plugin { p.Repo.ComponentTemplate = "{{"; return p }), + }, + { + name: "only one platform component", + valid: false, + p: trP(func(p *Plugin) *Plugin { p.Repo.Platforms = []string{"linux"}; return p }), + }, + { + name: "no image provided", + valid: false, + p: trP(func(p *Plugin) *Plugin { p.Repo.Name = ""; return p }), + }, + { + name: "no tags provided", + valid: false, + p: trP(func(p *Plugin) *Plugin { p.Repo.Tags = []string{}; return p }), + }, + { + name: "no registry name", + valid: false, + p: trP(func(p *Plugin) *Plugin { p.Registry.Name = ""; return p }), + }, + { + name: "invalid template variable", + valid: false, + p: trP(func(p *Plugin) *Plugin { + p.Repo.ComponentTemplate = "{{.Res}}" + return p + }), + }, + } + for _, tc := range testCases { + err := tc.p.Validate() + if err != nil && tc.valid { + t.Errorf("%s: expected valid plugin, but error was %v", tc.name, err) + } else if err == nil && !tc.valid { + t.Errorf("%s: expected invalid plugin, but error was nil", tc.name) + } else { + fmt.Printf("%s: Completed successfully: %v\n", tc.name, err) + } + } +} + +func makeDefaultPlugin() *Plugin { + return &Plugin{ + // build configuration + // registry configuration + Registry: &Registry{ + DryRun: true, + Name: "registry.example.com", + Username: "docker_user", + Password: "docker_pass", + PushRetry: 1, + }, + // repo configuration + Repo: &Repo{ + Name: "project/image", + Tags: []string{"latest", "v0.0.0"}, + Platforms: []string{"linux/amd64", "linux/arm64/v8"}, + ComponentTemplate: "{{.Repo}}:{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Variant}}-{{.Variant}}{{end}}", + }, + } +} + +// Translate Plugin +func trP(t func(*Plugin) *Plugin) *Plugin { + return t(makeDefaultPlugin()) +} diff --git a/cmd/vela-manifest-tool/registry.go b/cmd/vela-manifest-tool/registry.go new file mode 100644 index 0000000..325eb76 --- /dev/null +++ b/cmd/vela-manifest-tool/registry.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/base64" + "fmt" + + "github.com/spf13/afero" + + "github.com/sirupsen/logrus" +) + +const ( + credentials = `%s:%s` + + registryFile = `{ + "auths": { + "%s": { + "auth": "%s" + } + } +}` +) + +// Registry represents the plugin configuration for registry information. +// +// https://docs.docker.com/registry/ +type Registry struct { + // name of the registry to publish the image to + Name string + // user name for communication with the registry + Username string + // password for communication with the registry + Password string + // enable building the image without publishing + PushRetry int + // enable pulling from any insecure registry + DryRun bool +} + +// Write creates a Docker config.json file for building and publishing the image. +func (r *Registry) Write() error { + logrus.Trace("writing registry configuration file") + + // use custom filesystem which enables us to test + a := &afero.Afero{ + Fs: appFS, + } + + // check if name, username and password are provided + if len(r.Name) == 0 || len(r.Username) == 0 || len(r.Password) == 0 { + return nil + } + + // create basic authentication string for config.json file + basicAuth := base64.StdEncoding.EncodeToString( + []byte(fmt.Sprintf(credentials, r.Username, r.Password)), + ) + + // create output string for config.json file + out := fmt.Sprintf( + registryFile, + r.Name, + basicAuth, + ) + + // create full path for config.json file + path := "/root/.docker/config.json" + + //nolint: gomnd // ignore magic number + return a.WriteFile(path, []byte(out), 0644) +} + +// Validate verifies the Registry is properly configured. +func (r *Registry) Validate() error { + logrus.Trace("validating registry plugin configuration") + + // verify registry is provided + if len(r.Name) == 0 { + return fmt.Errorf("no registry name provided") + } + + // check if dry run is disabled + if !r.DryRun { + // check if username is provided + if len(r.Username) == 0 { + return fmt.Errorf("no registry username provided") + } + + // check if password is provided + if len(r.Password) == 0 { + return fmt.Errorf("no registry password provided") + } + } + + return nil +} diff --git a/cmd/vela-manifest-tool/registry_test.go b/cmd/vela-manifest-tool/registry_test.go new file mode 100644 index 0000000..94733f9 --- /dev/null +++ b/cmd/vela-manifest-tool/registry_test.go @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/spf13/afero" +) + +func TestDocker_Registry_Validate(t *testing.T) { + // setup types + r := &Registry{ + Name: "index.docker.io", + Username: "octocat", + Password: "superSecretPassword", + DryRun: false, + } + + err := r.Validate() + if err != nil { + t.Errorf("Validate returned err: %v", err) + } +} + +func TestDocker_Registry_Validate_NoName(t *testing.T) { + // setup types + r := &Registry{ + Username: "octocat", + Password: "superSecretPassword", + DryRun: false, + } + + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned err") + } +} + +func TestDocker_Registry_Validate_NoUsername(t *testing.T) { + // setup types + r := &Registry{ + Name: "index.docker.io", + Password: "superSecretPassword", + DryRun: false, + } + + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned err") + } +} + +func TestDocker_Registry_Validate_NoPassword(t *testing.T) { + // setup types + r := &Registry{ + Name: "index.docker.io", + Username: "octocat", + DryRun: false, + } + + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned err") + } +} + +func TestDocker_Registry_Write(t *testing.T) { + // setup filesystem + appFS = afero.NewMemMapFs() + + // setup types + r := &Registry{ + Name: "index.docker.io", + Username: "octocat", + Password: "superSecretPassword", + DryRun: false, + } + + err := r.Write() + if err != nil { + t.Errorf("Write returned err: %v", err) + } +} + +func TestDocker_Registry_Write_NoName(t *testing.T) { + // setup filesystem + appFS = afero.NewMemMapFs() + + // setup types + r := &Registry{ + Username: "octocat", + Password: "superSecretPassword", + DryRun: false, + } + + err := r.Write() + if err != nil { + t.Errorf("Write returned err: %v", err) + } +} + +func TestDocker_Registry_Write_NoUsername(t *testing.T) { + // setup filesystem + appFS = afero.NewMemMapFs() + + // setup types + r := &Registry{ + Name: "index.docker.io", + Username: "octocat", + DryRun: false, + } + + err := r.Write() + if err != nil { + t.Errorf("Write returned err: %v", err) + } +} + +func TestDocker_Registry_Write_NoPassword(t *testing.T) { + // setup filesystem + appFS = afero.NewMemMapFs() + + // setup types + r := &Registry{ + Name: "index.docker.io", + Username: "octocat", + DryRun: false, + } + + err := r.Write() + if err != nil { + t.Errorf("Write returned err: %v", err) + } +} diff --git a/cmd/vela-manifest-tool/repo.go b/cmd/vela-manifest-tool/repo.go new file mode 100644 index 0000000..ce2a4d5 --- /dev/null +++ b/cmd/vela-manifest-tool/repo.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + + "github.com/sirupsen/logrus" +) + +type ( + // Repo represents the plugin configuration for repo information + Repo struct { + Name string // name of the repository for the image + Tags []string // tags of the image for the repository + Platforms []string // platforms which should be included in the manifest + ComponentTemplate string // Template used to render each component image + } +) + +// Validate verifies the Repo is properly configured. +func (r *Repo) Validate() error { + logrus.Trace("validating repo plugin configuration") + + // verify repo is provided + if len(r.Name) == 0 { + return fmt.Errorf("no repo name provided") + } + + // check if tags are provided + if len(r.Tags) > 0 { + // check each tag value for valid docker tag syntax + for _, tag := range r.Tags { + if !tagRegexp.MatchString(tag) { + return fmt.Errorf(errTagValidation, tag) + } + } + } else { + return fmt.Errorf("no tags provided") + } + + if len(r.Platforms) > 0 { + for _, platform := range r.Platforms { + if _, ok := allowedPlatforms[platform]; !ok { + return fmt.Errorf("unsupported platform %s requested", platform) + } + } + } else { + return fmt.Errorf("no platforms provided") + } + + return nil +} diff --git a/cmd/vela-manifest-tool/repo_test.go b/cmd/vela-manifest-tool/repo_test.go new file mode 100644 index 0000000..7b4241c --- /dev/null +++ b/cmd/vela-manifest-tool/repo_test.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import "testing" + +func TestDocker_Repo_Validate(t *testing.T) { + // setup types + r := &Repo{ + Name: "/target/vela-manifest-tool", + Tags: []string{"latest"}, + Platforms: []string{"linux/amd64", "linux/arm64/v8"}, + } + + err := r.Validate() + if err != nil { + t.Errorf("Validate returned err: %v", err) + } +} + +func TestDocker_Repo_Validate_NoName(t *testing.T) { + // setup types + r := &Repo{ + Name: "", + Tags: []string{"latest"}, + Platforms: []string{"linux/amd64", "linux/arm64/v8"}, + } + + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned err") + } +} + +func TestDocker_Repo_Validate_InvalidTags(t *testing.T) { + // setup types + r := &Repo{ + Name: "/target/vela-manifest-tool", + Tags: []string{"!@#$%^&*()"}, + Platforms: []string{"linux/amd64", "linux/arm64/v8"}, + } + + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned err") + } +} + +func TestDocker_Repo_Validate_NoTags(t *testing.T) { + // setup types + r := &Repo{ + Name: "/target/vela-manifest-tool", + Tags: []string{}, + Platforms: []string{"linux/amd64", "linux/arm64/v8"}, + } + + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned err") + } +} + +func TestDocker_Repo_Validate_NoPlatforms(t *testing.T) { + r := &Repo{ + Name: "/target/vela-manifest-tool", + Tags: []string{"latest"}, + } + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned err") + } +} + +func TestDocker_Repo_InvalidPlatform(t *testing.T) { + r := &Repo{ + Name: "/target/vela-manifest-tool", + Tags: []string{"latest"}, + Platforms: []string{"windows/riscv64"}, + } + err := r.Validate() + if err == nil { + t.Errorf("Validate should have returned an err") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2ac3031 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/go-vela/vela-manifest-tool + +go 1.21 + +require ( + github.com/Masterminds/semver/v3 v3.2.1 + github.com/go-vela/types v0.23.0 + github.com/joho/godotenv v1.5.1 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/afero v1.11.0 + github.com/urfave/cli/v2 v2.27.1 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/psampaz/go-mod-outdated v0.9.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aafc04c --- /dev/null +++ b/go.sum @@ -0,0 +1,54 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-vela/types v0.23.0 h1:CWICreHO4V9KqbE+AINkRJVwCZmggxOLIZh+e1n/XXA= +github.com/go-vela/types v0.23.0/go.mod h1:AAqgxIw1aRBgPkE/5juGuiwh/JZuOtL8fcPaEkjFWwQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/psampaz/go-mod-outdated v0.9.0 h1:P3f6z6NrAgG1kq1W4xcsa/lL8SM2SEZxAlRvG1AMFBs= +github.com/psampaz/go-mod-outdated v0.9.0/go.mod h1:FcfE/igcl0GuLxemNXSL7r+rconnPmFP8kmO/Th3/To= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..44633a5 --- /dev/null +++ b/version/version.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +package version + +import ( + "fmt" + "runtime" + + "github.com/go-vela/types/version" + + "github.com/Masterminds/semver/v3" + + "github.com/sirupsen/logrus" +) + +var ( + // Arch represents the architecture information for the package. + Arch = runtime.GOARCH + // Commit represents the git commit information for the package. + Commit string + // Compiler represents the compiler information for the package. + Compiler = runtime.Compiler + // Date represents the build date information for the package. + Date string + // Go represents the golang version information for the package. + Go string + // OS represents the operating system information for the package. + OS = runtime.GOOS + // Tag represents the git tag information for the package. + Tag string +) + +// New creates a new version object for Vela that is used throughout the application. +func New() *version.Version { + // check if a semantic tag was provided + if len(Tag) == 0 { + logrus.Warning("no semantic tag provided - defaulting to v0.0.0") + + // set a fallback default for the tag + Tag = "v0.0.0" + } + + v, err := semver.NewVersion(Tag) + if err != nil { + fmt.Println(fmt.Errorf("unable to parse semantic version for %s: %w", Tag, err)) + return nil + } + + return &version.Version{ + Canonical: Tag, + Major: v.Major(), + Minor: v.Minor(), + Patch: v.Patch(), + PreRelease: v.Prerelease(), + Metadata: version.Metadata{ + Architecture: Arch, + BuildDate: Date, + Compiler: Compiler, + GitCommit: Commit, + GoVersion: Go, + OperatingSystem: OS, + }, + } +} From d753d95c04b74e3b7485011a777244cdca3ad534 Mon Sep 17 00:00:00 2001 From: Anthony Juckel Date: Fri, 21 Jun 2024 11:25:56 -0500 Subject: [PATCH 2/5] chore: go mod tidy --- go.mod | 4 ---- go.sum | 10 ---------- 2 files changed, 14 deletions(-) diff --git a/go.mod b/go.mod index 2ac3031..8282f92 100644 --- a/go.mod +++ b/go.mod @@ -15,10 +15,6 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/psampaz/go-mod-outdated v0.9.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum index aafc04c..3450319 100644 --- a/go.sum +++ b/go.sum @@ -14,18 +14,8 @@ github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/psampaz/go-mod-outdated v0.9.0 h1:P3f6z6NrAgG1kq1W4xcsa/lL8SM2SEZxAlRvG1AMFBs= -github.com/psampaz/go-mod-outdated v0.9.0/go.mod h1:FcfE/igcl0GuLxemNXSL7r+rconnPmFP8kmO/Th3/To= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= From b3d0d5dabe8a6b888ba33f5fbe1b608c8db252de Mon Sep 17 00:00:00 2001 From: Anthony Juckel Date: Mon, 8 Jul 2024 14:30:20 -0500 Subject: [PATCH 3/5] chore: cleanup golangci-lint suggestions --- cmd/vela-manifest-tool/command.go | 5 ++--- cmd/vela-manifest-tool/command_test.go | 13 +++++++++++-- cmd/vela-manifest-tool/main.go | 1 - cmd/vela-manifest-tool/main_test.go | 3 +++ cmd/vela-manifest-tool/manifestspec.go | 16 +++++++++++++++- cmd/vela-manifest-tool/manifestspec_test.go | 10 +++++++++- cmd/vela-manifest-tool/plugin.go | 19 ++++++++++++++----- cmd/vela-manifest-tool/plugin_test.go | 4 +++- cmd/vela-manifest-tool/repo.go | 2 +- cmd/vela-manifest-tool/repo_test.go | 2 ++ 10 files changed, 60 insertions(+), 15 deletions(-) diff --git a/cmd/vela-manifest-tool/command.go b/cmd/vela-manifest-tool/command.go index d384f6b..d1ef48e 100644 --- a/cmd/vela-manifest-tool/command.go +++ b/cmd/vela-manifest-tool/command.go @@ -14,12 +14,11 @@ import ( const manifestToolBin = "/manifest-tool" -// private variables just for test mocking +// Private variables just for test mocking. var stdout io.Writer = os.Stdout var stderr io.Writer = os.Stderr -// execCmd is a helper function to -// run the provided command. +// execCmd is a helper function to run the provided command. func execCmd(e *exec.Cmd) error { logrus.Tracef("executing cmd %s", strings.Join(e.Args, " ")) diff --git a/cmd/vela-manifest-tool/command_test.go b/cmd/vela-manifest-tool/command_test.go index 79e3a04..2e2f045 100644 --- a/cmd/vela-manifest-tool/command_test.go +++ b/cmd/vela-manifest-tool/command_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( @@ -15,6 +17,7 @@ func TestVersion(t *testing.T) { {cmd.Args[0], "manifest-tool"}, {cmd.Args[1], "--version"}, } + for _, tc := range cases { if !strings.Contains(tc.arg, tc.expected) { t.Errorf(`Expected %v to contain %q`, tc.arg, tc.expected) @@ -22,7 +25,7 @@ func TestVersion(t *testing.T) { } } -// Feels like execCmd should be written/tested in shared lib +// Feels like execCmd should be written/tested in shared lib. func TestExecution(t *testing.T) { cases := []struct { args []string @@ -30,22 +33,28 @@ func TestExecution(t *testing.T) { }{ {[]string{"echo", "-n", "foo"}, "foo", ""}, } + oldStdout := stdout defer func() { stdout = oldStdout }() + oldStderr := stderr defer func() { stderr = oldStderr }() + for _, tc := range cases { var outbuf, errbuf bytes.Buffer stdout, stderr = &outbuf, &errbuf - cmd := exec.Command(tc.args[0], tc.args[1:]...) + cmd := exec.Command(tc.args[0], tc.args[1:]...) //nolint:gosec // we control the test data + err := execCmd(cmd) if err != nil { t.Errorf("Expected no error when creating command: %v", err) } + if tc.expout != outbuf.String() { t.Errorf("Expected %q to be equal to %q", outbuf.String(), tc.expout) } + if tc.experr != errbuf.String() { t.Errorf("Expected %q to be equal to %q", errbuf.String(), tc.experr) } diff --git a/cmd/vela-manifest-tool/main.go b/cmd/vela-manifest-tool/main.go index acf1fd9..d7e1e55 100644 --- a/cmd/vela-manifest-tool/main.go +++ b/cmd/vela-manifest-tool/main.go @@ -16,7 +16,6 @@ import ( _ "github.com/joho/godotenv/autoload" ) -//nolint:funlen // ignore function length due to comments and flags func main() { v := version.New() diff --git a/cmd/vela-manifest-tool/main_test.go b/cmd/vela-manifest-tool/main_test.go index 05f2541..543401d 100644 --- a/cmd/vela-manifest-tool/main_test.go +++ b/cmd/vela-manifest-tool/main_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( @@ -15,6 +17,7 @@ func TestVersionCompatible(t *testing.T) { func TestVersionSemver(t *testing.T) { version.Tag = "abcd" + v := version.New() if v != nil { t.Errorf("version.New should return nil if a non-semver Tag (%q) is provided", version.Tag) diff --git a/cmd/vela-manifest-tool/manifestspec.go b/cmd/vela-manifest-tool/manifestspec.go index b15cede..0be6dfb 100644 --- a/cmd/vela-manifest-tool/manifestspec.go +++ b/cmd/vela-manifest-tool/manifestspec.go @@ -27,7 +27,7 @@ type Manifest struct { Template *template.Template } -// ManifestSpec represents the structure of the manifest-tool yaml spec file +// ManifestSpec represents the structure of the manifest-tool yaml spec file. type ManifestSpec struct { Image string // name of the image index including tag Manifests []ManifestComponent // list of component images to include in index @@ -55,6 +55,7 @@ type ComponentContext struct { func NewManifestSpec(reg *Registry, repo *Repo) ([]*ManifestSpec, error) { specs := []*ManifestSpec{} tmpl, err := template.New("component_template").Parse(repo.ComponentTemplate) + if err != nil { return specs, err } @@ -62,14 +63,17 @@ func NewManifestSpec(reg *Registry, repo *Repo) ([]*ManifestSpec, error) { if len(reg.Name) == 0 { return specs, fmt.Errorf("no registry name provided") } + if len(repo.Name) == 0 { return specs, fmt.Errorf("no repository name provided") } + for _, tag := range repo.Tags { ms := ManifestSpec{ Image: reg.Name + repo.Name + ":" + tag, Manifests: []ManifestComponent{}, } + for _, platform := range repo.Platforms { platformComp := strings.Split(platform, "/") if len(platformComp) < 2 { @@ -79,6 +83,7 @@ func NewManifestSpec(reg *Registry, repo *Repo) ([]*ManifestSpec, error) { // else to make the variant below clean platformComp = append(platformComp, "") } + ctx := ComponentContext{ Repo: repo.Name, Tag: tag, @@ -86,11 +91,14 @@ func NewManifestSpec(reg *Registry, repo *Repo) ([]*ManifestSpec, error) { Arch: platformComp[1], Variant: platformComp[2], } + var compImgBuf bytes.Buffer err = tmpl.Execute(&compImgBuf, ctx) + if err != nil { return specs, err } + compImg := compImgBuf.String() comp := ManifestComponent{ Image: fmt.Sprintf("%s%s", reg.Name, compImg), @@ -102,8 +110,10 @@ func NewManifestSpec(reg *Registry, repo *Repo) ([]*ManifestSpec, error) { } ms.Manifests = append(ms.Manifests, comp) } + specs = append(specs, &ms) } + return specs, nil } @@ -141,7 +151,9 @@ func (ms *ManifestSpec) Render(wr io.Writer) error { if err != nil { return err } + _, err = wr.Write(yamlData) + return err } @@ -151,8 +163,10 @@ func validateTagOfImage(fullImage string) error { if len(topLevelImgParts) != 2 { return fmt.Errorf("%s not in image:tag format", fullImage) } + if !tagRegexp.MatchString(topLevelImgParts[1]) { return fmt.Errorf(errTagValidation, topLevelImgParts[1]) } + return nil } diff --git a/cmd/vela-manifest-tool/manifestspec_test.go b/cmd/vela-manifest-tool/manifestspec_test.go index 1fde173..074e157 100644 --- a/cmd/vela-manifest-tool/manifestspec_test.go +++ b/cmd/vela-manifest-tool/manifestspec_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( @@ -19,11 +21,14 @@ func TestManifestSpec_New_Validate(t *testing.T) { man.Manifests[0].Image) assertImageMatch(t, "index.docker.io/octocat/hello-world:latest-linux-arm64-v8", man.Manifests[1].Image) + var data bytes.Buffer + err := man.Render(&data) if err != nil { t.Errorf("Error encountered during render: %v", err) } + expected := "image: index.docker.io/octocat/hello-world:latest\n" + "manifests:\n" + "- image: index.docker.io/octocat/hello-world:latest-linux-amd64\n" + @@ -143,6 +148,7 @@ func firstMS(ms []*ManifestSpec, _ error) *ManifestSpec { if len(ms) > 0 { return ms[0] } + return nil } @@ -170,13 +176,15 @@ func defaultFixture(t *testing.T) *ManifestSpec { if err != nil { t.Fatalf("error encountered: %v", err) } + if len(ms) != 1 { t.Fatalf("should only have returned a single manifest spec") } + return ms[0] } -// Translate ManifestSpec +// Translate ManifestSpec. func trMS(t *testing.T, f func(*ManifestSpec) *ManifestSpec) *ManifestSpec { return f(defaultFixture(t)) } diff --git a/cmd/vela-manifest-tool/plugin.go b/cmd/vela-manifest-tool/plugin.go index d28cbcb..22da5ac 100644 --- a/cmd/vela-manifest-tool/plugin.go +++ b/cmd/vela-manifest-tool/plugin.go @@ -35,9 +35,8 @@ type Plugin struct { manifestSpecs []*ManifestSpec // Parsed specs, populated as side effect of validate } -// Command formats and outputs the command necessary for -// manifest-tool to build and publish a Docker Manifest List or -// OCI Image Index +// Formats and outputs the command necessary for manifest-tool to build +// and publish a Docker Manifest List or OCI Image Index. func (p *Plugin) Command(specFile string) *exec.Cmd { logrus.Debug("creating manifest-tool command from plugin configuration") @@ -75,17 +74,21 @@ func (p *Plugin) Exec() error { if err != nil { return err } + a := &afero.Afero{ Fs: appFS, } + err = a.Mkdir("/root/specs", 0755) if err != nil { return err } for i, spec := range manifestSpecs { - fmt.Printf("Processing manifest list/image index %s\n", spec.Image) var data bytes.Buffer + + fmt.Printf("Processing manifest list/image index %s\n", spec.Image) + err = spec.Render(&data) if err != nil { return err @@ -93,7 +96,12 @@ func (p *Plugin) Exec() error { fmt.Printf("Rendered spec file:\n%s\n", data.String()) specFilename := fmt.Sprintf("/root/specs/spec_%d.yml", i) - a.WriteFile(specFilename, data.Bytes(), 0644) + + err = a.WriteFile(specFilename, data.Bytes(), 0644) + if err != nil { + return err + } + cmd := p.Command(specFilename) // If a dry run, return without executing the cmd if p.Registry.DryRun { @@ -139,6 +147,7 @@ func (p *Plugin) Validate() error { if err != nil { return err } + p.manifestSpecs = manifestSpecs return nil diff --git a/cmd/vela-manifest-tool/plugin_test.go b/cmd/vela-manifest-tool/plugin_test.go index e2116b8..e3ab066 100644 --- a/cmd/vela-manifest-tool/plugin_test.go +++ b/cmd/vela-manifest-tool/plugin_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( @@ -83,7 +85,7 @@ func makeDefaultPlugin() *Plugin { } } -// Translate Plugin +// Translate Plugin. func trP(t func(*Plugin) *Plugin) *Plugin { return t(makeDefaultPlugin()) } diff --git a/cmd/vela-manifest-tool/repo.go b/cmd/vela-manifest-tool/repo.go index ce2a4d5..a4c2cff 100644 --- a/cmd/vela-manifest-tool/repo.go +++ b/cmd/vela-manifest-tool/repo.go @@ -9,7 +9,7 @@ import ( ) type ( - // Repo represents the plugin configuration for repo information + // Repo represents the plugin configuration for repo information. Repo struct { Name string // name of the repository for the image Tags []string // tags of the image for the repository diff --git a/cmd/vela-manifest-tool/repo_test.go b/cmd/vela-manifest-tool/repo_test.go index 7b4241c..21abfcc 100644 --- a/cmd/vela-manifest-tool/repo_test.go +++ b/cmd/vela-manifest-tool/repo_test.go @@ -65,6 +65,7 @@ func TestDocker_Repo_Validate_NoPlatforms(t *testing.T) { Name: "/target/vela-manifest-tool", Tags: []string{"latest"}, } + err := r.Validate() if err == nil { t.Errorf("Validate should have returned err") @@ -77,6 +78,7 @@ func TestDocker_Repo_InvalidPlatform(t *testing.T) { Tags: []string{"latest"}, Platforms: []string{"windows/riscv64"}, } + err := r.Validate() if err == nil { t.Errorf("Validate should have returned an err") From d5de6dd31127f89c0e71a33fc56b41a4d0229451 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 29 Jun 2024 08:21:52 +0000 Subject: [PATCH 4/5] chore(deps): update all non-major dependencies --- .github/workflows/build.yml | 2 +- .github/workflows/codeql-analysis.yml | 8 ++++---- .github/workflows/prerelease.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/reviewdog.yml | 8 ++++---- .github/workflows/test.yml | 4 ++-- .github/workflows/validate.yml | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2f3f14..baa3561 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: steps: - name: clone - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: install go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9124d1d..1787d46 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4 + uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4 + uses: github/codeql-action/autobuild@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4 + uses: github/codeql-action/analyze@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 6b4eddf..202afe6 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -14,7 +14,7 @@ jobs: steps: - name: clone - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: # ensures we fetch tag history for the repository fetch-depth: 0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bd9a132..d19de25 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: steps: - name: clone - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: # ensures we fetch tag history for the repository fetch-depth: 0 diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index acebd32..d751a3a 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -12,7 +12,7 @@ jobs: steps: - name: clone - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: install go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 @@ -23,7 +23,7 @@ jobs: check-latest: true - name: golangci-lint - uses: reviewdog/action-golangci-lint@00311c26a97213f93f2fd3a3524d66762e956ae0 # v2.6.1 + uses: reviewdog/action-golangci-lint@7708105983c614f7a2725e2172908b7709d1c3e4 # v2.6.2 with: github_token: ${{ secrets.github_token }} golangci_lint_flags: "--config=.golangci.yml" @@ -36,7 +36,7 @@ jobs: steps: - name: clone - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: install go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 @@ -47,7 +47,7 @@ jobs: check-latest: true - name: golangci-lint - uses: reviewdog/action-golangci-lint@00311c26a97213f93f2fd3a3524d66762e956ae0 # v2.6.1 + uses: reviewdog/action-golangci-lint@7708105983c614f7a2725e2172908b7709d1c3e4 # v2.6.2 with: github_token: ${{ secrets.github_token }} golangci_lint_flags: "--config=.golangci.yml" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5a1988..b61a00f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: steps: - name: clone - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: install go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 @@ -28,7 +28,7 @@ jobs: go test -race -covermode=atomic -coverprofile=coverage.out ./... - name: coverage - uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # v4.3.1 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} file: coverage.out diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 03e1f6e..c67e0fd 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -13,7 +13,7 @@ jobs: steps: - name: clone - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: install go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 From 89e2806e95f141e4863efca56df2326785384a8d Mon Sep 17 00:00:00 2001 From: Anthony Juckel Date: Mon, 8 Jul 2024 14:38:50 -0500 Subject: [PATCH 5/5] chore: remove deprecated linters --- .golangci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f2bec79..a7e697c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -59,7 +59,6 @@ linters: - bidichk # checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - contextcheck # check the function whether use a non-inherited context - - deadcode # finds unused code - dupl # code clone detection - errcheck # checks for unchecked errors - errorlint # find misuses of errors @@ -85,14 +84,12 @@ linters: - nolintlint # reports ill-formed or insufficient nolint directives - revive # linter for go - staticcheck # applies static analysis checks, go vet on steroids - - structcheck # finds unused struct fields - stylecheck # replacement for golint - tenv # analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 - typecheck # parses and type-checks go code, like the front-end of a go compiler - unconvert # remove unnecessary type conversions - unparam # reports unused function parameters - unused # checks for unused constants, variables, functions and types - - varcheck # finds unused global variables and constants - whitespace # detects leading and trailing whitespace - wsl # forces code to use empty lines