From 828339c24e54cac6a8f9409b776fb0b0fc3fb08c Mon Sep 17 00:00:00 2001 From: Ruslan Aliev Date: Mon, 29 Jan 2024 10:43:21 -0600 Subject: [PATCH] Initial commit Change-Id: I7e9a2194a2e83a04dc3e91b26978e063c3c9b6aa Signed-off-by: Ruslan Aliev --- .gitignore | 26 ++ .zuul.yaml | 89 +++++ LICENSE | 201 ++++++++++ Makefile | 132 +++++++ README.md | 2 + cmd/apply.go | 45 +++ cmd/root.go | 81 ++++ cmd/server.go | 50 +++ crd.yaml | 242 ++++++++++++ go.mod | 72 ++++ go.sum | 211 ++++++++++ images/armada-go/Dockerfile.ubuntu_focal | 38 ++ main.go | 29 ++ pkg/apply/apply.go | 383 +++++++++++++++++++ pkg/config/config.go | 169 ++++++++ pkg/log/log.go | 92 +++++ pkg/server/server.go | 42 ++ pkg/util/homedir.go | 49 +++ pkg/wait/wait.go | 147 +++++++ tools/gate/playbooks/docker-image-build.yaml | 126 ++++++ tools/gate/playbooks/vars.yaml | 19 + tools/image_tags.py | 126 ++++++ 22 files changed, 2371 insertions(+) create mode 100644 .gitignore create mode 100644 .zuul.yaml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/apply.go create mode 100644 cmd/root.go create mode 100644 cmd/server.go create mode 100644 crd.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 images/armada-go/Dockerfile.ubuntu_focal create mode 100644 main.go create mode 100644 pkg/apply/apply.go create mode 100644 pkg/config/config.go create mode 100644 pkg/log/log.go create mode 100644 pkg/server/server.go create mode 100644 pkg/util/homedir.go create mode 100644 pkg/wait/wait.go create mode 100644 tools/gate/playbooks/docker-image-build.yaml create mode 100644 tools/gate/playbooks/vars.yaml create mode 100644 tools/image_tags.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f02333 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 0000000..7094fa9 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,89 @@ +# 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. + +- project: + check: + jobs: + - armada-go-docker-build-gate-ubuntu_focal + + gate: + jobs: + - armada-go-docker-build-gate-ubuntu_focal + + post: + jobs: + - armada-go-docker-publish-ubuntu_focal + + +- nodeset: + name: armada-go-single-node-focal + nodes: + - name: primary + label: ubuntu-focal + + +- job: + name: armada-go-docker-build-gate-ubuntu_focal + timeout: 3600 + run: tools/gate/playbooks/docker-image-build.yaml + nodeset: armada-go-single-node-focal + vars: + publish: false + distro: ubuntu_focal + tags: + dynamic: + patch_set: true + + +- job: + name: armada-go-docker-publish-ubuntu_focal + timeout: 3600 + run: tools/gate/playbooks/docker-image-build.yaml + nodeset: armada-go-single-node-focal + secrets: + - airship_armada_go_quay_creds + vars: + publish: true + distro: ubuntu_focal + tags: + dynamic: + branch: true + commit: true + static: + - latest + + +- secret: + name: airship_armada_go_quay_creds + data: + username: !encrypted/pkcs1-oaep + - WT0T3twqEWlkYWkvqmuWvvU/cYXMVurCPjELm0ilZqVBBj9traoHaUgURfYiNJSu6+u+X + vSugo9uaPgCXM5oQbAyWmtwDH+WHVOmdflx+Q2usmaym7JEET+PYh7DEKsrf3/YRtotn3 + 3r8hEXFbaq0+k5MJdc+byQ6CX2PS2W4I8TTRbH+jMDEDttEAXNDpG1C63Geol9deo7c2u + eQXdzTVcyZ+fn0/nb8bMdAjC+KpWCpU6O2/kMYStdDQYr105JYD1e36YVeGheSDAHqHIj + 1LDHqtjGMeiliimizZ08Lero/yjZiUpQCUGVYqyqi9Tpk5OmecJR9ub0W5pTBPmEMkbiN + R/8owZMczjWdhD7/Vej9rJAu8iHGnRMJoWYCtLD0De0q6OxQEpSWJl6c7aKsmRorAB3Jt + sM+GuVwEBllaj67l176Ql4T662zIBVaSUckcsVjQef+JMMj6CKgF+U43pylwFUk/ilnP5 + HJfo8AUtW1P5hpU8xLB4mcS1h5K0ynneWIydiB2aUBsQ34e0T/u1OLfKiLdMBaaPS4Olu + ks16VnS1VDNXlt0hKEylS4IX77oxtXnRnFpV/3Z3ohlrccvT23FiUr2k0maFTsqQYBs2F + KJxMTtI/x9G4kk7FtZKyg2w6BYhEgOhmajcqksfEGSgytmhugVq6RC22hHROTM= + password: !encrypted/pkcs1-oaep + - bNJ9MY8MvMpwIsvGmY9GsZz1b9XCUeOAlpvL/KAXRGT7sBjt15nvobF+Y/RXjGia+kq/C + Y8Yz8DEwHAZ9EjqsY20uhoIC89ArjziGxq+ZFl89HHvmWkgs+TYuBh9A0MZUiAOJNkJR4 + yQIWPYvfzknutFqZXGzQ2GPGvp+Lk45PZJs4io91+WQX1DT16fdWpB/CtVfZCrFOyLswM + 58f+K56wf+I2tly9KoAp83gYVE+mGwkqqBUdv6jNcnm9/UMxTGFj1zgKNJdF0sSpT1T1I + DwynMkKDcvJCsUoH0S0EO/JYd2ne5VsKrI5/4EU2gmBrvvQFyCqccjYHc3557TN+bAe0H + 6xtQSZfM31i/IiGBhR1fqQbyAWVmA27i5pmn5TcekI7YpELCeoIRY5M/BQ5slqpTvbp4C + VEez4iQvXoHepfRtpFN3/zATFolTqhCldJN+bh6wdg3d8lSecsetD6cU3Fm7aHJuLYoSr + a4xPhx6s7B1J6wq0P14ADGl5/0CmzeZ3uGw94PRGvg8YAWswUV/DXWogbOH/EEStdsOia + JaXpU/oVkmC5NcIp3A+6F3NCft8gQCTKecoFBt9/7suvjmCIY9y26SBQ0gOHEjbUzfikT + FuwYIUDaaLDU+Dz+PfdrKSM63O/uBhrDO+yo+ovqu5PPLksBu42sOrZleAbZIA= diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e4af3b1 --- /dev/null +++ b/Makefile @@ -0,0 +1,132 @@ +# APP INFO +BUILD_DIR := $(shell mktemp -d) +DOCKER_REGISTRY ?= quay.io +IMAGE_PREFIX ?= airshipit +IMAGE_NAME ?= armada-go +IMAGE_TAG ?= latest +PROXY ?= http://proxy.foo.com:8000 +NO_PROXY ?= localhost,127.0.0.1,.svc.cluster.local +USE_PROXY ?= false +PUSH_IMAGE ?= false +# use this variable for image labels added in internal build process +LABEL ?= org.airshipit.build=community +COMMIT ?= $(shell git rev-parse HEAD) +PYTHON = python3 +CHARTS := $(filter-out deps, $(patsubst charts/%/.,%,$(wildcard charts/*/.))) +DISTRO ?= ubuntu_focal +IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${IMAGE_NAME}:${IMAGE_TAG}-${DISTRO} +UBUNTU_BASE_IMAGE ?= +# VERSION INFO +GIT_COMMIT = $(shell git rev-parse HEAD) +GIT_SHA = $(shell git rev-parse --short HEAD) +GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) +GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") +ifdef VERSION + DOCKER_VERSION = $(VERSION) +endif +SHELL = /bin/bash +info: + @echo "Version: ${VERSION}" + @echo "Git Tag: ${GIT_TAG}" + @echo "Git Commit: ${GIT_COMMIT}" + @echo "Git Tree State: ${GIT_DIRTY}" + @echo "Docker Version: ${DOCKER_VERSION}" + @echo "Registry: ${DOCKER_REGISTRY}" + +# Image URL to use all building/pushing image targets +IMG ?= quay.io/airshipit/armada-go:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.28.0 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Build + +.PHONY: build +build: ## Build manager binary. + go build -o bin/armada-go main.go + +.PHONY: run +run: ## Run a controller from your host. + go run main.go + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +check-docker: + @if [ -z $$(which docker) ]; then \ + echo "Missing \`docker\` client which is required for development"; \ + exit 2; \ + fi +images: check-docker build_armada_go +_BASE_IMAGE_ARG := $(if $(UBUNTU_BASE_IMAGE),--build-arg FROM="${UBUNTU_BASE_IMAGE}" ,) +build_armada_go: +ifeq ($(USE_PROXY), true) + docker build --network host -t $(IMAGE) --label $(LABEL) \ + --label "org.opencontainers.image.revision=$(COMMIT)" \ + --label "org.opencontainers.image.created=$(shell date --rfc-3339=seconds --utc)" \ + --label "org.opencontainers.image.title=$(IMAGE_NAME)" \ + -f images/armada-go/Dockerfile.$(DISTRO) \ + $(_BASE_IMAGE_ARG) \ + --build-arg http_proxy=$(PROXY) \ + --build-arg https_proxy=$(PROXY) \ + --build-arg HTTP_PROXY=$(PROXY) \ + --build-arg HTTPS_PROXY=$(PROXY) \ + --build-arg no_proxy=$(NO_PROXY) \ + --build-arg NO_PROXY=$(NO_PROXY) . +else + docker build --network host -t $(IMAGE) --label $(LABEL) \ + --label "org.opencontainers.image.revision=$(COMMIT)" \ + --label "org.opencontainers.image.created=$(shell date --rfc-3339=seconds --utc)" \ + --label "org.opencontainers.image.title=$(IMAGE_NAME)" \ + -f images/armada-go/Dockerfile.$(DISTRO) \ + $(_BASE_IMAGE_ARG) \ + --build-arg HELM_ARTIFACT_URL=$(HELM_ARTIFACT_URL) . +endif +ifeq ($(PUSH_IMAGE), true) + docker push $(IMAGE) +endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..88b2f6c --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# armada-go +A Golang-based orchestrator for managing a collection of Kubernetes Helm charts diff --git a/cmd/apply.go b/cmd/apply.go new file mode 100644 index 0000000..859e2aa --- /dev/null +++ b/cmd/apply.go @@ -0,0 +1,45 @@ +/* + 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 + + https://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 cmd + +import ( + "github.com/spf13/cobra" + + "opendev.org/airship/armada-go/pkg/apply" + "opendev.org/airship/armada-go/pkg/config" +) + +// NewApplyCommand creates a command to apply armada manifests +func NewApplyCommand(cfgFactory config.Factory) *cobra.Command { + p := &apply.RunCommand{Factory: cfgFactory} + + runCmd := &cobra.Command{ + Use: "apply", + Short: "armada-go command to apply manifests", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + p.Manifests = args[0] + p.Out = cmd.OutOrStdout() + return p.RunE() + }, + } + + var metricsOutput string + flags := runCmd.Flags() + flags.StringVar(&p.TargetManifest, "target-manifest", "", "target manifest") + flags.StringVar(&metricsOutput, "metrics-output", "", "metrics output") + + return runCmd +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..262b997 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,81 @@ +/* + 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 + + https://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 cmd + +import ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + + cfg "opendev.org/airship/armada-go/pkg/config" + "opendev.org/airship/armada-go/pkg/log" +) + +const longRoot = `Armada-Go is a tool for managing multiple Helm charts with dependencies by centralizing +all configurations in a single Armada YAML and providing life-cycle hooks for all Helm releases.` + +// RootOptions stores global flags values +type RootOptions struct { + Debug bool + ArmadaConfigPath string +} + +// NewArmadaCommand creates a root `armada` command with the default commands attached +func NewArmadaCommand(out io.Writer) *cobra.Command { + rootCmd, settings := NewRootCommand(out) + return AddDefaultArmadaCommands(rootCmd, + cfg.CreateFactory(&settings.ArmadaConfigPath)) +} + +// NewRootCommand creates the root `armada` command. All other commands are +// subcommands branching from this one +func NewRootCommand(out io.Writer) (*cobra.Command, *RootOptions) { + options := &RootOptions{} + rootCmd := &cobra.Command{ + Use: "armada", + Short: "A Golang-based orchestrator for managing a collection of Kubernetes Helm charts", + Long: longRoot, + SilenceErrors: true, + SilenceUsage: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + log.Init(options.Debug, cmd.ErrOrStderr()) + }, + } + rootCmd.SetOut(out) + initFlags(options, rootCmd) + + return rootCmd, options +} + +// AddDefaultArmadaCommands is a convenience function for adding all the +// default commands to armada-go +func AddDefaultArmadaCommands(cmd *cobra.Command, factory cfg.Factory) *cobra.Command { + cmd.AddCommand(NewServerCommand(factory)) + cmd.AddCommand(NewApplyCommand(factory)) + + return cmd +} + +func initFlags(options *RootOptions, cmd *cobra.Command) { + flags := cmd.PersistentFlags() + flags.BoolVar(&options.Debug, "debug", false, "enable verbose output") + + defaultArmadaConfigDir := filepath.Join("$HOME", ".armada") + + defaultArmadaConfigPath := filepath.Join(defaultArmadaConfigDir, "config") + flags.StringVar(&options.ArmadaConfigPath, "armadaconf", "", + `path to the armada-go configuration file. Defaults to "`+defaultArmadaConfigPath+`"`) +} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..d587327 --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,50 @@ +/* + 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 + + https://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 cmd + +import ( + "github.com/spf13/cobra" + + "opendev.org/airship/armada-go/pkg/config" + "opendev.org/airship/armada-go/pkg/server" +) + +const ( + runLong = ` +Run armada-go in server mode +` + runExample = ` +Run armada-go server +# armada-go server +` +) + +// NewServerCommand creates a command to run specific phase +func NewServerCommand(cfgFactory config.Factory) *cobra.Command { + p := &server.RunCommand{Factory: cfgFactory} + + runCmd := &cobra.Command{ + Use: "server", + Short: "armada-go command to run server", + Long: runLong[1:], + Args: cobra.ExactArgs(0), + Example: runExample, + RunE: func(cmd *cobra.Command, args []string) error { + return p.RunE() + }, + } + + return runCmd +} diff --git a/crd.yaml b/crd.yaml new file mode 100644 index 0000000..1059a3b --- /dev/null +++ b/crd.yaml @@ -0,0 +1,242 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: armadacharts.armada.airshipit.org +spec: + group: armada.airshipit.org + names: + kind: ArmadaChart + listKind: ArmadaChartList + plural: armadacharts + singular: armadachart + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ArmadaChart is the Schema for the armadacharts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + data: + description: ArmadaChartSpec defines the specification of ArmadaChart + properties: + chart_name: + description: ChartName is name of ArmadaChart + type: string + namespace: + description: Namespace is a namespace for ArmadaChart + type: string + release: + description: Release is a name of corresponding Helm Release of ArmadaChart + type: string + source: + description: Source is a source location of Helm Chart *.tgz + properties: + location: + type: string + subpath: + type: string + type: + type: string + type: object + test: + description: Test holds the test parameters for this Helm release. + properties: + enabled: + description: Enabled is an example field of ArmadaChart. Edit + armadachart_types.go to remove/update + type: boolean + type: object + upgrade: + description: Upgrade holds the upgrade options for this Helm release. + properties: + pre: + properties: + delete: + items: + description: ArmadaChartDeleteResource defines the delete + options of ArmadaChart + properties: + labels: + additionalProperties: + type: string + type: object + type: + type: string + type: object + type: array + type: object + type: object + values: + description: Values holds the values for this Helm release. + x-kubernetes-preserve-unknown-fields: true + wait: + description: Wait holds the wait options for this Helm release. + properties: + labels: + additionalProperties: + type: string + type: object + native: + description: ArmadaChartWaitNative defines the wait options of + ArmadaChart + properties: + enabled: + type: boolean + type: object + resources: + items: + description: ArmadaChartWaitResource defines the wait options + of ArmadaChart + properties: + labels: + additionalProperties: + type: string + type: object + min_ready: + type: string + type: + type: string + type: object + type: array + timeout: + description: Timeout is the time to wait for full reconciliation + of Helm release. + type: integer + type: object + type: object + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + status: + description: ArmadaChartStatus defines the observed state of ArmadaChart + properties: + conditions: + description: Conditions holds the conditions for the ArmadaChart. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + failures: + description: Failures is the reconciliation failure count against + the latest desired state. It is reset after a successful reconciliation. + format: int64 + type: integer + helmChart: + description: HelmChart is the namespaced name of the HelmChart resource + created by the controller for the ArmadaChart. + type: string + installFailures: + description: InstallFailures is the install failure count against + the latest desired state. It is reset after a successful reconciliation. + format: int64 + type: integer + lastAppliedRevision: + description: LastAppliedRevision is the revision of the last successfully + applied source. + type: string + lastAttemptedRevision: + description: LastAttemptedRevision is the revision of the last reconciliation + attempt. + type: string + lastAttemptedValuesChecksum: + description: LastAttemptedValuesChecksum is the SHA1 checksum of the + values of the last reconciliation attempt. + type: string + lastReleaseRevision: + description: LastReleaseRevision is the revision of the last successful + Helm release. + type: integer + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + tested: + description: Tested is the bool value whether the Helm Release was + successfully tested or not. + type: boolean + upgradeFailures: + description: UpgradeFailures is the upgrade failure count against + the latest desired state. It is reset after a successful reconciliation. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dc733f1 --- /dev/null +++ b/go.mod @@ -0,0 +1,72 @@ +module opendev.org/airship/armada-go + +go 1.20 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/go-logr/logr v1.2.4 + github.com/spf13/cobra v1.7.0 + golang.org/x/sync v0.3.0 + k8s.io/api v0.28.4 + k8s.io/apiextensions-apiserver v0.28.3 + k8s.io/apimachinery v0.28.4 + k8s.io/client-go v0.28.4 + k8s.io/klog/v2 v2.100.1 + opendev.org/airship/armada-operator v0.0.0-20240131234736-165edb913367 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect + sigs.k8s.io/controller-runtime v0.16.3 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5267f93 --- /dev/null +++ b/go.sum @@ -0,0 +1,211 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.0/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= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU= +k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +opendev.org/airship/armada-operator v0.0.0-20240131234736-165edb913367 h1:Ae/TMu6lXzpF4KlLGpshu6oL9BZ8OwYYNGNvnLyBnVw= +opendev.org/airship/armada-operator v0.0.0-20240131234736-165edb913367/go.mod h1:0hmfND6t5ZuyHB3R7TmsAreqzF4vfc/q3LwX18gFldI= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/images/armada-go/Dockerfile.ubuntu_focal b/images/armada-go/Dockerfile.ubuntu_focal new file mode 100644 index 0000000..8c8a0ad --- /dev/null +++ b/images/armada-go/Dockerfile.ubuntu_focal @@ -0,0 +1,38 @@ +ARG FROM=ubuntu:20.04 +ARG GO_IMAGE=golang:1.20.5-buster +FROM ${GO_IMAGE} as builder + +ENV PATH "/usr/local/go/bin:$PATH" +ENV CGO_ENABLED=0 +WORKDIR /go/src/ +COPY go.mod /go.sum ./ +RUN go mod download +COPY . ./ +RUN go build -v -o /usr/local/bin/armada-go ./ + +FROM ${FROM} as release + +LABEL org.opencontainers.image.authors='airship-discuss@lists.airshipit.org, irc://#airshipit@freenode' \ + org.opencontainers.image.url='https://airshipit.org' \ + org.opencontainers.image.documentation='https://docs.airshipit.org/armada-go' \ + org.opencontainers.image.source='https://opendev.org/airship/armada-go' \ + org.opencontainers.image.vendor='The Airship Authors' \ + org.opencontainers.image.licenses='Apache-2.0' + +ENV DEBIAN_FRONTEND noninteractive +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +EXPOSE 8000 + +WORKDIR /armada +COPY --from=builder /usr/local/bin/armada-go /usr/local-bin/armada +COPY crd.yaml /armada/crd.yaml + +# Add armada user +RUN useradd -u 1000 -g users -d $(pwd) armada + +ENTRYPOINT ["/usr/local/bin/armada"] +CMD ["server"] + +USER armada diff --git a/main.go b/main.go new file mode 100644 index 0000000..9d9ea38 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +/* + 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 + + https://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 ( + "fmt" + "os" + + "opendev.org/airship/armada-go/cmd" +) + +func main() { + if err := cmd.NewArmadaCommand(os.Stdout).Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/pkg/apply/apply.go b/pkg/apply/apply.go new file mode 100644 index 0000000..0d51fe4 --- /dev/null +++ b/pkg/apply/apply.go @@ -0,0 +1,383 @@ +/* + 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 + + https://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 apply + +import ( + "bufio" + "context" + "errors" + "flag" + "fmt" + "io" + "os" + "strings" + "time" + + "golang.org/x/sync/errgroup" + v1 "k8s.io/api/core/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextension "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" + "sigs.k8s.io/yaml" + + "opendev.org/airship/armada-go/pkg/config" + armadawait "opendev.org/airship/armada-go/pkg/wait" + armadav1 "opendev.org/airship/armada-operator/api/v1" +) + +// RunCommand phase run command +type RunCommand struct { + Factory config.Factory + Manifests string + TargetManifest string + Out io.Writer + + airManifest *AirshipManifest + airGroups map[string]*AirshipChartGroup + airCharts map[string]*AirshipChart +} + +type AirshipDocument struct { + Schema string `json:"schema,omitempty"` + Metadata AirshipMetadata `json:"metadata,omitempty"` +} + +type AirshipMetadata struct { + Name string `json:"name,omitempty"` +} + +type AirshipManifest struct { + AirshipDocument + AirshipManifestSpec `json:"data,omitempty"` +} + +type AirshipManifestSpec struct { + ChartGroups []string `json:"chart_groups,omitempty"` + ReleasePrefix string `json:"release_prefix,omitempty"` +} + +type AirshipChartGroup struct { + AirshipDocument + AirshipChartGroupSpec `json:"data,omitempty"` +} + +type AirshipChartGroupSpec struct { + ChartGroup []string `json:"chart_group,omitempty"` + Description string `json:"description,omitempty"` + Sequenced bool `json:"sequenced,omitempty"` +} + +type AirshipChart struct { + AirshipDocument + armadav1.ArmadaChartSpec `json:"data,omitempty"` +} + +// RunE runs the phase +func (c *RunCommand) RunE() error { + klog.InitFlags(nil) + klog.SetOutput(c.Out) + if err := flag.Set("v", "5"); err != nil { + return err + } + klog.V(2).Infof("armada-go apply, manifests path %s", c.Manifests) + + if err := c.ParseManifests(); err != nil { + return err + } + + k8sConfig, err := rest.InClusterConfig() + if err != nil { + klog.V(2).Infoln("Unable to load in-cluster kubeconfig, reason: ", err) + k8sConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).ClientConfig() + if err != nil { + return err + } + } + + if err := c.VerifyNamespaces(k8sConfig); err != nil { + return err + } + + dc := dynamic.NewForConfigOrDie(k8sConfig) + resClient := dc.Resource(schema.GroupVersionResource{ + Group: armadav1.ArmadaChartGroup, + Version: armadav1.ArmadaChartVersion, + Resource: armadav1.ArmadaChartPlural, + }) + + acClient := armadav1.NewForConfigOrDie(k8sConfig) + + if err := c.CheckCRD(k8sConfig); err != nil { + return err + } + + for _, cgName := range c.airManifest.ChartGroups { + cg := c.airGroups[cgName] + klog.V(5).Infof("processing chart group %s, sequenced %s", cgName, cg.Sequenced) + if !cg.Sequenced { + eg := errgroup.Group{} + for _, cName := range cg.ChartGroup { + klog.V(5).Infof("adding 1 chart to wg %s", cName) + chp := c.airCharts[cName] + chpc := c.ConvertChart(chp) + eg.Go(func() error { + return c.InstallChart(chpc, resClient, acClient) + }) + } + if err := eg.Wait(); err != nil { + return err + } + } else { + for _, cName := range cg.ChartGroup { + klog.V(5).Infof("sequential chart install %s", cName) + if err = c.InstallChart(c.ConvertChart(c.airCharts[cName]), resClient, acClient); err != nil { + return err + } + } + } + } + return nil +} + +func (c *RunCommand) InstallChart( + chart *armadav1.ArmadaChart, + resClient dynamic.NamespaceableResourceInterface, + restConfig *rest.RESTClient) error { + + klog.V(5).Infof("installing chart %s %s %s", chart.GetName(), chart.Name, chart.Namespace) + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(chart) + if err != nil { + return err + } + + if oldObj, err := resClient.Namespace(chart.Namespace).Get( + context.Background(), chart.GetName(), metav1.GetOptions{}); err != nil { + klog.V(5).Infof("unable to get chart %s: %s, creating", chart.Name, err.Error()) + if _, err = resClient.Namespace(chart.Namespace).Create( + context.Background(), &unstructured.Unstructured{Object: obj}, metav1.CreateOptions{}); err != nil { + return err + } + klog.V(5).Infof("chart has been successfully created %s", chart.Name) + } else { + uObj := &unstructured.Unstructured{Object: obj} + uObj.SetResourceVersion(oldObj.GetResourceVersion()) + klog.V(5).Infof("chart %s was found, updating", chart.Name) + if _, err = resClient.Namespace(chart.Namespace).Update( + context.Background(), uObj, metav1.UpdateOptions{}); err != nil { + klog.V(5).Infof("resource update error: %s", err.Error()) + if strings.Contains(err.Error(), "the object has been modified") { + klog.V(5).Infof("resource expired, retrying %s", err.Error()) + return c.InstallChart(chart, resClient, restConfig) + } + return err + } + klog.V(5).Infof("chart has been successfully updated %s", chart.Name) + } + + wOpts := armadawait.WaitOptions{ + Getter: restConfig, + Namespace: chart.Namespace, + LabelSelector: fmt.Sprintf("%s=%s", armadav1.ArmadaChartLabel, + fmt.Sprintf("%s-%s", c.airManifest.ReleasePrefix, chart.Spec.Release)), + ResourceType: "armadacharts.armada.airshipit.io", + Timeout: time.Second * time.Duration(chart.Spec.Wait.Timeout), + Logger: klog.FromContext(context.Background()), + } + + err = wOpts.Wait(context.Background()) + klog.V(5).Infof("finished with chart %s", chart.GetName()) + return err +} + +func (c *RunCommand) ConvertChart(chart *AirshipChart) *armadav1.ArmadaChart { + return &armadav1.ArmadaChart{ + TypeMeta: metav1.TypeMeta{ + Kind: armadav1.ArmadaChartKind, + APIVersion: armadav1.ArmadaChartAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", c.airManifest.ReleasePrefix, chart.Release), + Namespace: chart.Namespace, + Labels: map[string]string{ + armadav1.ArmadaChartLabel: fmt.Sprintf("%s-%s", c.airManifest.ReleasePrefix, chart.Release), + }, + }, + Spec: chart.ArmadaChartSpec, + } +} + +func (c *RunCommand) CheckCRD(restConfig *rest.Config) error { + crdClient := apiextension.NewForConfigOrDie(restConfig) + if _, err := crdClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.Background(), "armadacharts.armada.airshipit.io", metav1.GetOptions{}); err != nil { + if apierrors.IsNotFound(err) { + klog.V(5).Infof("armadacharts CRD not found, creating: %s", err.Error()) + objToapp, err := c.ReadCRD() + if err != nil { + return err + } + _, err = crdClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.Background(), objToapp, metav1.CreateOptions{}) + if err != nil { + klog.V(5).Infof("error while creating crd %t", err) + return err + } + } else { + return err + } + } + return nil +} + +func (c *RunCommand) ReadCRD() (*apiextv1.CustomResourceDefinition, error) { + sch := runtime.NewScheme() + _ = scheme.AddToScheme(sch) + _ = apiextv1.AddToScheme(sch) + + decode := serializer.NewCodecFactory(sch).UniversalDeserializer().Decode + data, err := os.ReadFile("crd.yaml") + if err != nil { + return nil, err + } + obj, _, err := decode(data, nil, nil) + if err != nil { + return nil, err + } + + crdTo := obj.(*apiextv1.CustomResourceDefinition) + return crdTo, nil +} + +func (c *RunCommand) VerifyNamespaces(rsc *rest.Config) error { + cs := kubernetes.NewForConfigOrDie(rsc) + + namespaces := make(map[string]bool) + for _, cgname := range c.airManifest.ChartGroups { + cg := c.airGroups[cgname] + for _, chrt := range cg.ChartGroup { + ns := c.airCharts[chrt].Namespace + if _, ok := namespaces[ns]; !ok { + namespaces[ns] = true + } + } + } + for k, _ := range namespaces { + klog.V(5).Infof("processing namespace %s", k) + if _, err := cs.CoreV1().Namespaces().Get(context.Background(), k, metav1.GetOptions{}); err != nil { + if apierrors.IsNotFound(err) { + klog.V(5).Infof("namespace %s not found, creating", k) + if _, err = cs.CoreV1().Namespaces().Create(context.Background(), &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: k}}, metav1.CreateOptions{}); err != nil { + return err + } + } else { + return err + } + } + } + klog.V(5).Infof("all namespaces validated successfully") + return nil +} + +func (c *RunCommand) ValidateManifests() error { + if c.airManifest == nil { + return errors.New("no or multiple armada manifest found") + } + + for _, cgname := range c.airManifest.ChartGroups { + if cg, ok := c.airGroups[cgname]; ok { + for _, cName := range cg.ChartGroup { + if chrt, ok := c.airCharts[cName]; ok { + if chrt.Release == "" || chrt.Namespace == "" { + return errors.New(fmt.Sprintf("chart document with name %s found does not have release or ns", cName)) + } + } else { + return errors.New(fmt.Sprintf("no chart document with name %s found", cName)) + } + } + } else { + return errors.New(fmt.Sprintf("no group document with name %s found", cgname)) + } + } + klog.V(5).Infof("all airship manifests validated successfully") + return nil +} + +func (c *RunCommand) ParseManifests() error { + klog.V(5).Infof("parsing manifests started, path: %s", c.Manifests) + f, err := os.Open(c.Manifests) + if err != nil { + return err + } + defer f.Close() + + c.airCharts = map[string]*AirshipChart{} + c.airGroups = map[string]*AirshipChartGroup{} + multidocReader := utilyaml.NewYAMLReader(bufio.NewReader(f)) + for { + buf, err := multidocReader.Read() + if err != nil { + if err == io.EOF { + break + } + return err + } + var typeMeta AirshipDocument + if err := yaml.Unmarshal(buf, &typeMeta); err != nil { + klog.V(2).Infof("unmarshalling error %s, continuing...", err.Error()) + continue + } + + if typeMeta.Schema == "armada/Manifest/v1" { + if (c.TargetManifest != "" && typeMeta.Metadata.Name == c.TargetManifest) || + (c.TargetManifest == "" && c.airManifest == nil) { + var airManifest AirshipManifest + if err := yaml.Unmarshal(buf, &airManifest); err != nil { + return err + } + klog.V(2).Infof("found airship manifest %s", airManifest.Metadata.Name) + c.airManifest = &airManifest + } + } + if typeMeta.Schema == "armada/ChartGroup/v1" { + var cg AirshipChartGroup + if err := yaml.Unmarshal(buf, &cg); err != nil { + return err + } + c.airGroups[typeMeta.Metadata.Name] = &cg + } + + if typeMeta.Schema == "armada/Chart/v1" { + var chrt AirshipChart + if err := yaml.Unmarshal(buf, &chrt); err != nil { + return err + } + c.airCharts[typeMeta.Metadata.Name] = &chrt + } + } + + return c.ValidateManifests() +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..1b67aa6 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,169 @@ +/* + 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 + https://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 config + +import ( + "fmt" + "os" + "path/filepath" + + "sigs.k8s.io/yaml" + + "opendev.org/airship/armada-go/pkg/log" + "opendev.org/airship/armada-go/pkg/util" +) + +// Where possible, json tags match the cli argument names. +// Top level config objects and all values required for proper functioning are not "omitempty". +// Any truly optional piece of config is allowed to be omitted. + +// Config holds the information required by armada-go commands +// It is somewhat a superset of what a kubeconfig looks like +type Config struct { + // +optional + Kind string `json:"kind,omitempty"` + + // loadedConfigPath is the full path to the location of the config + // file from which this config was loaded + // +not persisted in file + loadedConfigPath string + //fileSystem kustfs.FileSystem +} + +// Factory is a function which returns ready to use config object and error (if any) +type Factory func() (*Config, error) + +// CreateFactory returns function which creates ready to use Config object +func CreateFactory(armadaConfigPath *string) Factory { + return func() (*Config, error) { + cfg := NewEmptyConfig() + + var acp string + if armadaConfigPath != nil { + acp = *armadaConfigPath + } + + cfg.initConfigPath(acp) + err := cfg.LoadConfig() + if err != nil { + // Should stop armada-go + log.Print("Failed to load or initialize config: ", err) + CreateConfig(acp, true) + } + + return cfg, nil + } +} + +// CreateConfig saves default config to the specified path +func CreateConfig(armadaConfigPath string, overwrite bool) error { + cfg := NewConfig() + cfg.initConfigPath(armadaConfigPath) + return cfg.PersistConfig(overwrite) +} + +// initConfigPath - Initializes loadedConfigPath variable for Config object +func (c *Config) initConfigPath(armadaConfigPath string) { + switch { + case armadaConfigPath != "": + // The loadedConfigPath may already have been received as a command line argument + c.loadedConfigPath = armadaConfigPath + case os.Getenv("ARMADA_CONFIG") != "": + // Otherwise, we can check if we got the path via ENVIRONMENT variable + c.loadedConfigPath = os.Getenv("ARMADA_CONFIG") + default: + // Otherwise, we'll try putting it in the home directory + c.loadedConfigPath = filepath.Join(util.UserHomeDir(), ".armada", "config") + } +} + +func (c *Config) LoadConfig() error { + // If I can read from the file, load from it + // throw an error otherwise + data, err := os.ReadFile(c.loadedConfigPath) + if err != nil { + return err + } + + return yaml.Unmarshal(data, c) +} + +// NewEmptyConfig returns an initialized Config object with no default values +func NewEmptyConfig() *Config { + return &Config{} +} + +// NewConfig returns a newly initialized Config object +func NewConfig() *Config { + return &Config{ + Kind: "kind", + //fileSystem: kustfs.MakeFsInMemory(), + } +} + +// ErrConfigFileExists is returned when there is an existing file at specified location +type ErrConfigFileExists struct { + Path string +} + +func (e ErrConfigFileExists) Error() string { + return fmt.Sprintf("could not create default config at %s, file already exists", e.Path) +} + +// ToYaml returns a YAML document +// It serializes the given Config object to a valid YAML document +func (c *Config) ToYaml() ([]byte, error) { + return yaml.Marshal(&c) +} + +// PersistConfig updates the airshipctl config file to match +// the current Config object. +// If file did not previously exist, the file will be created. +// The file will be overwritten if overwrite argument set to true +func (c *Config) PersistConfig(overwrite bool) error { + if _, err := os.Stat(c.loadedConfigPath); err == nil && !overwrite { + return ErrConfigFileExists{Path: c.loadedConfigPath} + } + + airshipConfigYaml, err := c.ToYaml() + if err != nil { + return err + } + + // WriteFile doesn't create the directory, create it if needed + dir := filepath.Dir(c.loadedConfigPath) + err = os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + // Change the permission of directory + err = os.Chmod(dir, os.FileMode(0755)) + if err != nil { + return err + } + + // Write the Airship Config file + err = os.WriteFile(c.loadedConfigPath, airshipConfigYaml, 0644) + if err != nil { + return err + } + + // Change the permission of config file + err = os.Chmod(c.loadedConfigPath, os.FileMode(0644)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..d57dd95 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,92 @@ +/* + 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 + + https://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 log + +import ( + "fmt" + "io" + "log" + "os" +) + +var ( + debug = false + armadaLog = log.New(os.Stderr, "[armada-go] ", log.LstdFlags) +) + +// Init initializes settings related to logging +func Init(debugFlag bool, out io.Writer) { + debug = debugFlag + if debug { + armadaLog.SetFlags(log.LstdFlags | log.Llongfile) + } + armadaLog.SetOutput(out) +} + +// DebugEnabled returns whether the debug level is set +func DebugEnabled() bool { + return debug +} + +// Debug is a wrapper for log.Debug +func Debug(v ...interface{}) { + if debug { + writeLog(v...) + } +} + +// Debugf is a wrapper for log.Debugf +func Debugf(format string, v ...interface{}) { + if debug { + writeLog(fmt.Sprintf(format, v...)) + } +} + +// Print is a wrapper for log.Print +func Print(v ...interface{}) { + writeLog(v...) +} + +// Printf is a wrapper for log.Printf +func Printf(format string, v ...interface{}) { + writeLog(fmt.Sprintf(format, v...)) +} + +// Fatal is a wrapper for log.Fatal +func Fatal(v ...interface{}) { + armadaLog.Fatal(v...) +} + +// Fatalf is a wrapper for log.Fatalf +func Fatalf(format string, v ...interface{}) { + armadaLog.Fatalf(format, v...) +} + +// Writer returns log output writer object +func Writer() io.Writer { + return armadaLog.Writer() +} + +func writeLog(v ...interface{}) { + if debug { + err := armadaLog.Output(3, fmt.Sprint(v...)) + if err != nil { + log.Print(v...) + log.Print(err) + } + } else { + armadaLog.Print(v...) + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..4b23b29 --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,42 @@ +/* + 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 + + https://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 server + +import ( + "github.com/gin-gonic/gin" + "opendev.org/airship/armada-go/pkg/config" + "opendev.org/airship/armada-go/pkg/log" +) + +// RunCommand phase run command +type RunCommand struct { + Factory config.Factory +} + +func Apply(c *gin.Context) { +} + +// RunE runs the phase +func (c *RunCommand) RunE() error { + _, err := c.Factory() + if err != nil { + return err + } + + log.Printf("armada-go server has been started") + r := gin.Default() + r.GET("/apply", Apply) + return r.Run(":8000") +} diff --git a/pkg/util/homedir.go b/pkg/util/homedir.go new file mode 100644 index 0000000..09835d3 --- /dev/null +++ b/pkg/util/homedir.go @@ -0,0 +1,49 @@ +/* + 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 + + https://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 util + +import ( + "os" + "path/filepath" + "strings" +) + +// UserHomeDir is a utility function that wraps os.UserHomeDir and returns no +// errors. If the user has no home directory, the returned value will be the +// empty string +func UserHomeDir() string { + homeDir, err := os.UserHomeDir() + if err != nil { + homeDir = "" + } + return homeDir +} + +// ExpandTilde expands the path to include the home directory if the path +// is prefixed with `~`. If it isn't prefixed with `~` or has no slash after tilde, +// the path is returned as-is. +func ExpandTilde(path string) string { + // Just tilde - return current $HOME dir + if path == "~" { + return UserHomeDir() + } + // If path starts with ~/ - expand it + if strings.HasPrefix(path, "~/") { + return filepath.Join(UserHomeDir(), path[1:]) + } + + // empty strings, absolute paths, ~<(dir/file)name> return as-is + return path +} diff --git a/pkg/wait/wait.go b/pkg/wait/wait.go new file mode 100644 index 0000000..1bc13dd --- /dev/null +++ b/pkg/wait/wait.go @@ -0,0 +1,147 @@ +/* + 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 + + https://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 wait + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" + watchtools "k8s.io/client-go/tools/watch" + + armadav1 "opendev.org/airship/armada-operator/api/v1" +) + +type StatusType string + +type Status struct { + StatusType + Msg string +} + +const ( + Ready StatusType = "READY" + Skipped StatusType = "SKIPPED" + Unready StatusType = "UNREADY" + Error StatusType = "ERROR" +) + +// WaitOptions phase run command +type WaitOptions struct { + Getter cache.Getter + Namespace string + LabelSelector string + ResourceType string + Timeout time.Duration + Logger logr.Logger +} + +func getObjectStatus(obj interface{}) Status { + switch v := obj.(type) { + case *armadav1.ArmadaChart: + return isArmadaChartReady(v) + default: + return Status{Error, fmt.Sprintf("Unable to cast an object to any type %s\n", obj)} + } +} + +func allMatch(logger logr.Logger, store cache.Store, obj runtime.Object) (bool, error) { + for _, item := range store.List() { + if obj != nil && item == obj { + continue + } + status := getObjectStatus(item) + logger.Info(fmt.Sprintf("all match object %T is ready returned %s\n", item, status.StatusType)) + logger.Info(status.Msg) + if status.StatusType != Ready && status.StatusType != Skipped { + logger.Info(fmt.Sprintf("all match exiting false due to %s\n", status.StatusType)) + return false, nil + } + } + logger.Info("all objects are ready\n") + return true, nil +} + +func processEvent(logger logr.Logger, event watch.Event) (StatusType, error) { + metaObj, err := meta.Accessor(event.Object) + if err != nil { + return Error, err + } + + logger.Info(fmt.Sprintf("watch event: type=%s, name=%s, namespace=%s, resource_ver %s", + event.Type, metaObj.GetName(), metaObj.GetNamespace(), metaObj.GetResourceVersion())) + + if event.Type == "ERROR" { + return Error, errors.New(fmt.Sprintf("resource %s: got error event %s", metaObj.GetName(), event.Object)) + } + + status := getObjectStatus(event.Object) + logger.Info(fmt.Sprintf("object type: %T, status: %s", event.Object, status.Msg)) + return status.StatusType, nil +} + +func isArmadaChartReady(ac *armadav1.ArmadaChart) Status { + if ac.Status.ObservedGeneration == ac.Generation { + for _, cond := range ac.Status.Conditions { + if cond.Type == "Ready" && cond.Status == "True" { + return Status{Ready, fmt.Sprintf("armadachart %s ready", ac.GetName())} + } + } + } + return Status{Unready, fmt.Sprintf("Waiting for armadachart %s to be ready", ac.GetName())} +} + +// Wait runs the phase +func (c *WaitOptions) Wait(parent context.Context) error { + c.Logger.Info(fmt.Sprintf("armada-go wait , namespace %s labels %s type %s timeout %s", c.Namespace, c.LabelSelector, c.ResourceType, c.Timeout)) + + ctx, cancelFunc := watchtools.ContextWithOptionalTimeout(parent, c.Timeout) + defer cancelFunc() + + lw := cache.NewFilteredListWatchFromClient(c.Getter, "armadacharts", c.Namespace, func(options *metav1.ListOptions) { + options.LabelSelector = c.LabelSelector + c.Logger.Info(fmt.Sprintf("Label selector applied %s", options)) + }) + + var cacheStore cache.Store + + cpu := func(store cache.Store) (bool, error) { + cacheStore = store + if len(store.List()) == 0 { + c.Logger.Info(fmt.Sprintf("skipping non-required wait, no resources found.\n")) + return true, nil + } + return allMatch(c.Logger, cacheStore, nil) + } + + cfu := func(event watch.Event) (bool, error) { + if ready, err := processEvent(c.Logger, event); ready != Ready || err != nil { + return false, err + } + + return allMatch(c.Logger, cacheStore, event.Object) + } + + _, err := watchtools.UntilWithSync(ctx, lw, nil, cpu, cfu) + c.Logger.Info(fmt.Sprintf("wait completed %s\n", c.LabelSelector)) + return err +} diff --git a/tools/gate/playbooks/docker-image-build.yaml b/tools/gate/playbooks/docker-image-build.yaml new file mode 100644 index 0000000..73bebf9 --- /dev/null +++ b/tools/gate/playbooks/docker-image-build.yaml @@ -0,0 +1,126 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# 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. + +- hosts: primary + roles: + - bindep + - ensure-docker + - ensure-python + - ensure-pip + + tasks: + - include_vars: vars.yaml + + - name: Debug tag generation inputs + block: + - debug: + var: publish + - debug: + var: distro + - debug: + var: tags + - debug: + var: zuul + - debug: + msg: "{{ tags | to_json }}" + + - name: Determine tags + shell: echo '{{ tags | to_json }}' | python3 {{ zuul.project.src_dir }}/tools/image_tags.py + environment: + BRANCH: "{{ zuul.branch | default('') }}" + CHANGE: "{{ zuul.change | default('') }}" + COMMIT: "{{ zuul.newrev | default('') }}" + PATCHSET: "{{ zuul.patchset | default('') }}" + register: image_tags + + - name: Debug computed tags + debug: + var: image_tags + + - name: Install Docker python module for ansible docker login + block: + - pip: + name: docker + version: 4.4.4 + executable: pip3 + become: True + + - name: Install tox python module for ansible docker login + block: + - pip: + name: tox + version: 3.28.0 + executable: pip3 + become: True + + +# - name: Run images +# when: not publish +# shell: | +# set -ex +# sudo -E -H pip3 install tox==3.28.0 +# make run_images +# args: +# chdir: "{{ zuul.project.src_dir }}" +# executable: /bin/bash +# become: True + + + + - name: Make images + when: not publish + block: + - make: + chdir: "{{ zuul.project.src_dir }}" + target: images + params: + DISTRO: "{{ distro }}" + IMAGE_TAG: "{{ item }}" + with_items: "{{ image_tags.stdout_lines }}" + + - shell: "docker images" + register: docker_images + + - debug: + var: docker_images + + become: True + + - name: Publish images + block: + - docker_login: + username: "{{ airship_armada_go_quay_creds.username }}" + password: "{{ airship_armada_go_quay_creds.password }}" + registry_url: "https://quay.io/api/v1/" + + - make: + chdir: "{{ zuul.project.src_dir }}" + target: images + params: + DOCKER_REGISTRY: "quay.io" + IMAGE_PREFIX: "airshipit" + DISTRO: "{{ distro }}" + IMAGE_TAG: "{{ item }}" + COMMIT: "{{ zuul.newrev | default('') }}" + PUSH_IMAGE: "true" + with_items: "{{ image_tags.stdout_lines }}" + + - shell: "docker images" + register: docker_images + + - debug: + var: docker_images + + when: publish + become: True diff --git a/tools/gate/playbooks/vars.yaml b/tools/gate/playbooks/vars.yaml new file mode 100644 index 0000000..c89b798 --- /dev/null +++ b/tools/gate/playbooks/vars.yaml @@ -0,0 +1,19 @@ +# Copyright 2017 The Openstack-Helm Authors. +# +# 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. + +docker_daemon: + group: zuul + registry-mirrors: + - "http://{{ zuul_site_mirror_fqdn }}:8082/" + storage-driver: overlay2 diff --git a/tools/image_tags.py b/tools/image_tags.py new file mode 100644 index 0000000..be28669 --- /dev/null +++ b/tools/image_tags.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# 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. + +import json +import logging +import os +import sys + +LOG = logging.getLogger(__name__) + +LOG_FORMAT = '%(asctime)s %(levelname)-8s %(name)s:%(filename)s:%(lineno)3d:%(funcName)s %(message)s' # noqa + + +class TagGenExeception(Exception): + pass + + +def read_config(stream, env): + config = {} + try: + config['tags'] = json.load(stream) + except ValueError: + LOG.exception('Failed to decode JSON from input stream') + config['tags'] = {} + + LOG.debug('Configuration after reading stream: %s', config) + + config['context'] = { + 'branch': env.get('BRANCH'), + 'change': env.get('CHANGE'), + 'commit': env.get('COMMIT'), + 'ps': env.get('PATCHSET'), + } + + LOG.info('Final configuration: %s', config) + + return config + + +def build_tags(config): + tags = config.get('tags', {}).get('static', []) + LOG.debug('Dynamic tags: %s', tags) + tags.extend(build_dynamic_tags(config)) + LOG.info('All tags: %s', tags) + return tags + + +def build_dynamic_tags(config): + dynamic_tags = [] + + dynamic_tags.extend(_build_branch_tag(config)) + dynamic_tags.extend(_build_commit_tag(config)) + dynamic_tags.extend(_build_ps_tag(config)) + + return dynamic_tags + + +def _build_branch_tag(config): + if _valid_dg(config, 'branch'): + return [config['context']['branch']] + else: + return [] + + +def _build_commit_tag(config): + if _valid_dg(config, 'commit'): + return [config['context']['commit']] + else: + return [] + + +def _build_ps_tag(config): + if _valid_dg(config, 'patch_set', 'change') and _valid_dg( + config, 'patch_set', 'ps'): + return [ + '%s-%s' % (config['context']['change'], config['context']['ps']) + ] + else: + return [] + + +def _valid_dg(config, dynamic_tag, context_name=None): + if context_name is None: + context_name = dynamic_tag + + if config.get('tags', {}).get('dynamic', {}).get(dynamic_tag): + if config.get('context', {}).get(context_name): + return True + else: + raise TagGenExeception('Dynamic tag "%s" requested, but "%s"' + ' not found in context' % (dynamic_tag, + context_name)) + else: + return False + + +def main(): + config = read_config(sys.stdin, os.environ) + tags = build_tags(config) + + for tag in tags: + print(tag) + + +if __name__ == '__main__': + logging.basicConfig(format=LOG_FORMAT, level=logging.WARNING) + try: + main() + except TagGenExeception: + LOG.exception('Failed to generate tags') + sys.exit(1) + except Exception: + LOG.exception('Unexpected exception') + sys.exit(2)