├── .bazelrc ├── .bazelrc.travis ├── .editorconfig ├── .gitignore ├── .travis.yml ├── BUILD.bazel ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── WORKSPACE ├── api.go ├── build ├── buildozer_commands.txt ├── code-generator │ └── boilerplate.go.txt ├── golangcilint.yaml ├── nogo-config.json └── print-workspace-status.sh ├── cmd ├── crd │ ├── BUILD.bazel │ └── main.go └── smith │ ├── BUILD.bazel │ ├── app │ ├── BUILD.bazel │ └── bundle_controller.go │ └── main.go ├── cover.sh ├── docs ├── deployment │ ├── 0-crd.yaml │ ├── 1-common-rbac.yaml │ ├── 2-cluster-wide-access-setup.yaml │ ├── 2-namespaced-access-setup.yaml │ └── 3-deployment.yaml └── design │ ├── authorization.md │ ├── field-references.md │ ├── managing-resources.md │ ├── object-references.md │ ├── plugins.md │ └── soft-deletes.md ├── examples ├── service_catalog │ ├── README.md │ ├── application.yaml │ ├── cleanup.sh │ └── img │ │ ├── Application_example.png │ │ ├── Kubernetes_graph.png │ │ └── asciinema.png └── sleeper │ ├── BUILD.bazel │ ├── app.go │ ├── client.go │ ├── main │ ├── BUILD.bazel │ └── main.go │ ├── pkg │ └── apis │ │ └── sleeper │ │ └── v1 │ │ ├── BUILD.bazel │ │ ├── register.go │ │ ├── types.go │ │ ├── types_test.go │ │ └── zz_generated.deepcopy.go │ └── sleeper_event_handler.go ├── go.mod ├── go.sum ├── it ├── BUILD.bazel ├── adoption_test.go ├── crd_attribute_test.go ├── deployment_dependencies_test.go ├── deployment_ready_test.go ├── main_test.go ├── resource_deletion_test.go ├── sc │ ├── BUILD.bazel │ ├── instance_and_binding_depend_on_secret_test.go │ ├── main_test.go │ ├── service_catalog_test.go │ ├── ups-clusterservicebroker.yaml │ └── zz_objects_for_test.go ├── update_test.go ├── utils_for_tests.go └── workflow_test.go ├── misc ├── bundle_template.yaml └── sleepers.yaml ├── pkg ├── apis │ └── smith │ │ ├── BUILD.bazel │ │ ├── register.go │ │ └── v1 │ │ ├── BUILD.bazel │ │ ├── doc.go │ │ ├── register.go │ │ ├── types.go │ │ ├── types_test.go │ │ └── zz_generated.deepcopy.go ├── client │ ├── BUILD.bazel │ ├── bundle.go │ ├── clientset_generated │ │ └── clientset │ │ │ ├── BUILD.bazel │ │ │ ├── clientset.go │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── BUILD.bazel │ │ │ ├── clientset_generated.go │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ ├── scheme │ │ │ ├── BUILD.bazel │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ └── typed │ │ │ └── smith │ │ │ └── v1 │ │ │ ├── BUILD.bazel │ │ │ ├── bundle.go │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── BUILD.bazel │ │ │ ├── doc.go │ │ │ ├── fake_bundle.go │ │ │ └── fake_smith_client.go │ │ │ ├── generated_expansion.go │ │ │ └── smith_client.go │ └── smart │ │ ├── BUILD.bazel │ │ ├── discovery.go │ │ ├── discovery_test.go │ │ └── smart.go ├── controller │ ├── bundlec │ │ ├── BUILD.bazel │ │ ├── bundle_sync_task.go │ │ ├── controller.go │ │ ├── controller_crd_event_handler.go │ │ ├── controller_worker.go │ │ ├── controller_worker_test.go │ │ ├── finalizers.go │ │ ├── resource_sync_task.go │ │ ├── spec_processor.go │ │ ├── spec_processor_test.go │ │ └── types.go │ └── bundlec_test │ │ ├── BUILD.bazel │ │ ├── actual_object_passed_to_plugin_test.go │ │ ├── cleanup_test.go │ │ ├── cr_in_another_namespace_test.go │ │ ├── delay_postpone_delete_removed_object_test.go │ │ ├── delay_proceed_delete_removed_object_test.go │ │ ├── delay_start_delete_removed_object_test.go │ │ ├── delete_removed_object_test.go │ │ ├── deleted_bundle_foreground_deletion_noop_test.go │ │ ├── deleted_bundle_manual_delete_resources_fail_test.go │ │ ├── deleted_bundle_manual_delete_resources_success_test.go │ │ ├── deleted_bundle_remove_finalizer_test.go │ │ ├── detect_infinite_update_cycles_test.go │ │ ├── finalizer_added_if_not_present_test.go │ │ ├── invalid_depends_on_test.go │ │ ├── no_actions_for_blocked_resources_test.go │ │ ├── no_deletions_while_in_progress_test.go │ │ ├── not_marked_crd_ignored_test.go │ │ ├── owner_references_test.go │ │ ├── plugin_error_propagated_test.go │ │ ├── plugin_schema_invalid_test.go │ │ ├── plugin_spec_processed_test.go │ │ ├── processing_continues_after_error_test.go │ │ ├── prohibited_annotations_object_test.go │ │ ├── prohibited_annotations_plugin_test.go │ │ ├── propagate_status_test.go │ │ ├── resolve_binding_secret_references_test.go │ │ ├── schema_early_validation_test.go │ │ ├── secret_keys_not_merged_test.go │ │ ├── service_instance_schema_invalid_test.go │ │ ├── two_resources_same_name_test.go │ │ ├── zz_objects_for_test.go │ │ ├── zz_plugins_for_test.go │ │ └── zz_plumbing_for_test.go ├── crd │ ├── BUILD.bazel │ └── crd.go ├── plugin │ ├── BUILD.bazel │ ├── plugin.go │ └── types.go ├── resources │ ├── BUILD.bazel │ ├── crd_helpers.go │ ├── objects.go │ └── objects_test.go ├── specchecker │ ├── BUILD.bazel │ ├── builtin │ │ ├── BUILD.bazel │ │ ├── known_types.go │ │ ├── process_deployment.go │ │ ├── process_deployment_test.go │ │ ├── process_secret.go │ │ ├── process_service.go │ │ ├── process_service_binding.go │ │ ├── process_service_instance.go │ │ ├── process_service_instance_test.go │ │ └── process_util.go │ ├── checker.go │ ├── checker_test.go │ ├── hash.go │ ├── testing │ │ ├── BUILD.bazel │ │ └── stuff_for_tests.go │ └── types.go ├── statuschecker │ ├── BUILD.bazel │ ├── builtin │ │ ├── BUILD.bazel │ │ └── known_types.go │ └── checker.go ├── store │ ├── BUILD.bazel │ ├── bundle.go │ ├── catalog.go │ ├── crd.go │ ├── multi.go │ └── multi_basic.go └── util │ ├── BUILD.bazel │ ├── graph │ ├── BUILD.bazel │ ├── topological_sort.go │ ├── topological_sort_test.go │ └── types.go │ ├── logz │ ├── BUILD.bazel │ └── logz.go │ ├── testing │ ├── BUILD.bazel │ └── utils_for_tests.go │ └── util.go └── tools.go /.bazelrc: -------------------------------------------------------------------------------- 1 | build --workspace_status_command build/print-workspace-status.sh 2 | 3 | test --test_output=errors 4 | test --test_verbose_timeout_warnings 5 | -------------------------------------------------------------------------------- /.bazelrc.travis: -------------------------------------------------------------------------------- 1 | build --verbose_failures 2 | build --workspace_status_command build/print-workspace-status.sh 3 | build --curses=no 4 | build --show_timestamps 5 | 6 | test --test_output=all 7 | test --test_verbose_timeout_warnings 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [**] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | 11 | [**.go] 12 | insert_final_newline = true 13 | indent_style = tab 14 | 15 | [Makefile] 16 | insert_final_newline = true 17 | indent_style = tab 18 | 19 | [**.{yml,yaml,md}] 20 | insert_final_newline = true 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [BUILD.bazel] 25 | insert_final_newline = true 26 | indent_style = space 27 | indent_size = 4 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | /.vscode/ 4 | /bazel-bin 5 | /bazel-genfiles 6 | /bazel-out 7 | /bazel-smith 8 | /bazel-testlogs 9 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | # Disable build files generation for these directories 2 | # gazelle:exclude vendor/github.com/bazelbuild/buildtools/buildifier2 3 | # gazelle:exclude vendor/golang.org/x/tools/cmd/fiximports/testdata 4 | # gazelle:exclude vendor/golang.org/x/tools/go/gcimporter15/testdata 5 | # gazelle:exclude vendor/golang.org/x/tools/go/internal/gccgoimporter/testdata 6 | # gazelle:exclude vendor/golang.org/x/tools/go/loader/testdata 7 | # gazelle:exclude vendor/golang.org/x/tools/go/internal/gcimporter/testdata 8 | # gazelle:proto disable_global 9 | 10 | load("@bazel_gazelle//:def.bzl", "gazelle") 11 | load("@com_github_atlassian_bazel_tools//buildozer:def.bzl", "buildozer") 12 | load("@com_github_atlassian_bazel_tools//goimports:def.bzl", "goimports") 13 | load("@com_github_atlassian_bazel_tools//golangcilint:def.bzl", "golangcilint") 14 | load("@com_github_bazelbuild_buildtools//buildifier:def.bzl", "buildifier") 15 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo") 16 | 17 | gazelle( 18 | name = "gazelle", 19 | external = "vendored", 20 | gazelle = "@bazel_gazelle//cmd/gazelle:gazelle_pure", 21 | prefix = "github.com/atlassian/smith", 22 | ) 23 | 24 | gazelle( 25 | name = "gazelle_fix", 26 | command = "fix", 27 | external = "vendored", 28 | gazelle = "@bazel_gazelle//cmd/gazelle:gazelle_pure", 29 | prefix = "github.com/atlassian/smith", 30 | ) 31 | 32 | go_library( 33 | name = "go_default_library", 34 | srcs = ["api.go"], 35 | importpath = "github.com/atlassian/smith", 36 | visibility = ["//visibility:public"], 37 | ) 38 | 39 | buildifier( 40 | name = "buildifier", 41 | exclude_patterns = ["./vendor/*"], 42 | ) 43 | 44 | buildifier( 45 | name = "buildifier_check", 46 | exclude_patterns = ["./vendor/*"], 47 | mode = "check", 48 | ) 49 | 50 | buildifier( 51 | name = "buildifier_fix", 52 | lint_mode = "fix", 53 | ) 54 | 55 | buildozer( 56 | name = "buildozer", 57 | commands = "build/buildozer_commands.txt", 58 | ) 59 | 60 | goimports( 61 | name = "goimports", 62 | display_diffs = True, 63 | exclude_files = [ 64 | "zz_generated.*", 65 | ], 66 | exclude_paths = [ 67 | "./vendor/*", 68 | "./pkg/client/clientset_generated/*", 69 | ], 70 | prefix = "github.com/atlassian/smith", 71 | write = True, 72 | ) 73 | 74 | golangcilint( 75 | name = "golangcilint", 76 | config = "build/golangcilint.yaml", 77 | paths = [ 78 | ".", 79 | "cmd/...", 80 | "examples/...", 81 | "it/...", 82 | "pkg/...", 83 | ], 84 | prefix = "github.com/atlassian/smith", 85 | ) 86 | 87 | nogo( 88 | name = "nogo", 89 | config = "build/nogo-config.json", 90 | vet = True, 91 | visibility = ["//visibility:public"], 92 | ) 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v1.0.0 2 | ------ 3 | - implementation of plugins support 4 | - lots of internals refactoring 5 | - per-resource status reporting 6 | - bugfixes and improvements 7 | 8 | v0.2.3 9 | ------ 10 | - plugin support doc and plugin types 11 | 12 | v0.2.2 13 | ------ 14 | - refactoring and cleanups 15 | - proper plan name and service name handling 16 | 17 | v0.2.1 18 | ------ 19 | - informers respect namespace set on smith command line 20 | 21 | v0.2.0 22 | ------ 23 | - use Kubernetes 1.8 libraries 24 | - update Service Catalog to v0.1.0-rc2 25 | - add `pprof-address` flag to enable pprof on the address 26 | 27 | v0.1.3 28 | ------ 29 | - fix `service-catalog-insecure` flag not working 30 | 31 | v0.1.2 32 | ------ 33 | - workaround glog mess 34 | 35 | v0.1.1 36 | ------ 37 | - `disable-service-catalog` and `service-catalog-insecure` flags to configure interaction with Service Catalog 38 | 39 | 0.1.0 40 | ----- 41 | - Initial release 42 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "com_github_atlassian_smith") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | http_archive( 6 | name = "io_bazel_rules_go", 7 | sha256 = "b9aa86ec08a292b97ec4591cf578e020b35f98e12173bbd4a921f84f583aebd9", 8 | urls = [ 9 | "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.20.2/rules_go-v0.20.2.tar.gz", 10 | "https://github.com/bazelbuild/rules_go/releases/download/v0.20.2/rules_go-v0.20.2.tar.gz", 11 | ], 12 | ) 13 | 14 | http_archive( 15 | name = "bazel_gazelle", 16 | sha256 = "41bff2a0b32b02f20c227d234aa25ef3783998e5453f7eade929704dcff7cd4b", 17 | urls = [ 18 | "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/bazel-gazelle/releases/download/v0.19.0/bazel-gazelle-v0.19.0.tar.gz", 19 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.19.0/bazel-gazelle-v0.19.0.tar.gz", 20 | ], 21 | ) 22 | 23 | http_archive( 24 | name = "io_bazel_rules_docker", 25 | sha256 = "14ac30773fdb393ddec90e158c9ec7ebb3f8a4fd533ec2abbfd8789ad81a284b", 26 | strip_prefix = "rules_docker-0.12.1", 27 | urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.12.1/rules_docker-v0.12.1.tar.gz"], 28 | ) 29 | 30 | http_archive( 31 | name = "com_github_bazelbuild_buildtools", 32 | sha256 = "f3ef44916e6be705ae862c0520bac6834dd2ff1d4ac7e5abc61fe9f12ce7a865", 33 | strip_prefix = "buildtools-0.29.0", 34 | urls = ["https://github.com/bazelbuild/buildtools/archive/0.29.0.tar.gz"], 35 | ) 36 | 37 | http_archive( 38 | name = "com_google_protobuf", 39 | sha256 = "758249b537abba2f21ebc2d02555bf080917f0f2f88f4cbe2903e0e28c4187ed", 40 | strip_prefix = "protobuf-3.10.0", 41 | urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.10.0.tar.gz"], 42 | ) 43 | 44 | http_archive( 45 | name = "com_github_atlassian_bazel_tools", 46 | sha256 = "60821f298a7399450b51b9020394904bbad477c18718d2ad6c789f231e5b8b45", 47 | strip_prefix = "bazel-tools-a2138311856f55add11cd7009a5abc8d4fd6f163", 48 | urls = ["https://github.com/atlassian/bazel-tools/archive/a2138311856f55add11cd7009a5abc8d4fd6f163.tar.gz"], 49 | ) 50 | 51 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 52 | 53 | go_rules_dependencies() 54 | 55 | go_register_toolchains(nogo = "@//:nogo") 56 | 57 | load("@com_github_bazelbuild_buildtools//buildifier:deps.bzl", "buildifier_dependencies") 58 | load("@com_github_atlassian_bazel_tools//buildozer:deps.bzl", "buildozer_dependencies") 59 | load("@com_github_atlassian_bazel_tools//goimports:deps.bzl", "goimports_dependencies") 60 | load("@com_github_atlassian_bazel_tools//golangcilint:deps.bzl", "golangcilint_dependencies") 61 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") 62 | load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") 63 | load( 64 | "@io_bazel_rules_docker//repositories:repositories.bzl", 65 | container_repositories = "repositories", 66 | ) 67 | load( 68 | "@io_bazel_rules_docker//go:image.bzl", 69 | go_image_repositories = "repositories", 70 | ) 71 | 72 | gazelle_dependencies() 73 | 74 | goimports_dependencies() 75 | 76 | container_repositories() 77 | 78 | go_image_repositories() 79 | 80 | buildifier_dependencies() 81 | 82 | buildozer_dependencies() 83 | 84 | golangcilint_dependencies() 85 | 86 | protobuf_deps() 87 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package smith 2 | 3 | const ( 4 | Smith = "smith" 5 | Domain = "smith.atlassian.com" 6 | 7 | // See docs/design/managing-resources.md 8 | CrFieldPathAnnotation = Domain + "/CrReadyWhenFieldPath" 9 | CrFieldValueAnnotation = Domain + "/CrReadyWhenFieldValue" 10 | CrdSupportEnabled = Domain + "/SupportEnabled" 11 | 12 | EventAnnotationResourceName = Domain + "/ResourceName" 13 | EventAnnotationReason = Domain + "/Reason" 14 | 15 | DeletionDelayAnnotation = Domain + "/deletionDelay" 16 | DeletionTimestampAnnotation = Domain + "/deletionTimestamp" 17 | 18 | EventReasonResourceInProgress = "ResourceInProgress" 19 | EventReasonResourceReady = "ResourceReady" 20 | EventReasonResourceError = "ResourceError" 21 | EventReasonBundleInProgress = "BundleInProgress" 22 | EventReasonBundleReady = "BundleReady" 23 | EventReasonBundleError = "BundleError" 24 | EventReasonUnknown = "Unknown" 25 | ) 26 | -------------------------------------------------------------------------------- /build/buildozer_commands.txt: -------------------------------------------------------------------------------- 1 | set race "on"|//...:%go_test 2 | fix unusedLoads|//...:__pkg__ 3 | -------------------------------------------------------------------------------- /build/code-generator/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | -------------------------------------------------------------------------------- /build/golangcilint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # missing configuration 3 | # --aggregate 4 | # --sort=path,line 5 | # --vendor=true 6 | # --warnunmatcheddirective=false 7 | 8 | # missing linters 9 | # - go tool vet --shadow 10 | # - deadcode 11 | # - gotype 12 | # - unconvert 13 | 14 | run: 15 | concurrency: 4 16 | deadline: 10m 17 | skip-dirs: 18 | - "apis" 19 | - "client" 20 | skip-files: 21 | - "_test\\.go" 22 | - "\\.deepcopy\\.go" 23 | tests: false 24 | linters: 25 | enable: 26 | #- dupl 27 | #- gochecknoglobals 28 | - goconst 29 | - gocritic 30 | - gofmt 31 | #- golint 32 | - gosec 33 | - maligned 34 | - misspell 35 | - nakedret 36 | #- prealloc 37 | #- unparam 38 | disable: 39 | - gocyclo 40 | - interfacer 41 | - govet 42 | - staticcheck 43 | -------------------------------------------------------------------------------- /build/nogo-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "printf": { 3 | "exclude_files": { 4 | "/vendor/": "no need to vet third party code" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /build/print-workspace-status.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This command is used by bazel as the workspace_status_command 4 | # to implement build stamping with git information. 5 | 6 | set -o errexit 7 | set -o nounset 8 | set -o pipefail 9 | 10 | GIT_COMMIT=$(git rev-parse --short HEAD) 11 | GIT_TAG=$(git describe --abbrev=0 --tags 2>/dev/null || echo "0.0.0") 12 | 13 | # Prefix with STABLE_ so that these values are saved to stable-status.txt 14 | # instead of volatile-status.txt. 15 | # Stamped rules will be retriggered by changes to stable-status.txt, but not by 16 | # changes to volatile-status.txt. 17 | cat < coverage.out 10 | 11 | # Initialize error tracking 12 | ERROR="" 13 | 14 | # Test each package and append coverage profile info to coverage.out 15 | for pkg in `find . -depth -name \*.go | 16 | grep -ve '^./vendor/' | 17 | grep -ve '^./it/' | 18 | sed -e 's/^\.\/\(\(.*\)\/\)\{0,1\}[^/]*$/github.com\/atlassian\/smith\/\2/' | 19 | sort -u` 20 | do 21 | echo "Testing $pkg" 22 | go test -v -covermode=count -coverprofile=coverage_tmp.out "$pkg" || ERROR="Error testing $pkg" 23 | tail -n +2 coverage_tmp.out >> coverage.out 2> /dev/null ||: 24 | done 25 | 26 | rm -f coverage_tmp.out 27 | 28 | if [ ! -z "$ERROR" ] 29 | then 30 | die "Encountered error, last error was: $ERROR" 31 | fi 32 | -------------------------------------------------------------------------------- /docs/deployment/1-common-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: smith 5 | namespace: smith 6 | 7 | # Leader Election role 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: Role 11 | metadata: 12 | name: smith:leader-locking 13 | namespace: smith 14 | rules: 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - configmaps 19 | - events 20 | verbs: 21 | - create 22 | - apiGroups: 23 | - "" 24 | resources: 25 | - configmaps 26 | resourceNames: 27 | - smith-leader-elect 28 | verbs: 29 | - get 30 | - update 31 | 32 | # Leader Election binding 33 | --- 34 | apiVersion: rbac.authorization.k8s.io/v1 35 | kind: RoleBinding 36 | metadata: 37 | name: smith:leader-locking 38 | namespace: smith 39 | roleRef: 40 | apiGroup: rbac.authorization.k8s.io 41 | kind: Role 42 | name: "smith:leader-locking" 43 | subjects: 44 | - kind: ServiceAccount 45 | name: smith 46 | namespace: smith 47 | -------------------------------------------------------------------------------- /docs/deployment/2-cluster-wide-access-setup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: smith 6 | rules: 7 | 8 | - apiGroups: 9 | - apiextensions.k8s.io 10 | resources: 11 | - customresourcedefinitions 12 | verbs: 13 | - list 14 | - watch 15 | 16 | - apiGroups: 17 | - smith.atlassian.com 18 | resources: 19 | - bundles 20 | verbs: 21 | - list 22 | - watch 23 | - update # need to be able to update finalizers 24 | 25 | - apiGroups: 26 | - smith.atlassian.com 27 | resources: 28 | - bundles/status 29 | verbs: 30 | - update 31 | 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - configmaps 36 | - secrets 37 | - services 38 | - serviceaccounts 39 | verbs: 40 | - list 41 | - watch 42 | - create 43 | - update 44 | - delete 45 | 46 | - apiGroups: 47 | - apps 48 | resources: 49 | - deployments 50 | verbs: 51 | - list 52 | - watch 53 | - create 54 | - update 55 | - delete 56 | 57 | - apiGroups: 58 | - extensions 59 | resources: 60 | - ingresses 61 | verbs: 62 | - list 63 | - watch 64 | - create 65 | - update 66 | - delete 67 | 68 | - apiGroups: 69 | - autoscaling 70 | resources: 71 | - horizontalpodautoscalers 72 | verbs: 73 | - list 74 | - watch 75 | - create 76 | - update 77 | - delete 78 | 79 | - apiGroups: 80 | - servicecatalog.k8s.io 81 | resources: 82 | - servicebindings 83 | - serviceinstances 84 | verbs: 85 | - list 86 | - watch 87 | - create 88 | - update 89 | - delete 90 | --- 91 | apiVersion: rbac.authorization.k8s.io/v1 92 | kind: ClusterRoleBinding 93 | metadata: 94 | name: smith 95 | roleRef: 96 | apiGroup: rbac.authorization.k8s.io 97 | kind: ClusterRole 98 | name: smith 99 | subjects: 100 | - kind: ServiceAccount 101 | name: smith 102 | namespace: smith 103 | -------------------------------------------------------------------------------- /docs/deployment/2-namespaced-access-setup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: smith-cluster-role 6 | rules: 7 | 8 | - apiGroups: 9 | - apiextensions.k8s.io 10 | resources: 11 | - customresourcedefinitions 12 | verbs: 13 | - list 14 | - watch 15 | --- 16 | apiVersion: rbac.authorization.k8s.io/v1 17 | kind: ClusterRole # cluster wide role but it is bound only in a specific namespace (or multiple) 18 | metadata: 19 | name: smith-namespaced-role 20 | rules: 21 | 22 | - apiGroups: 23 | - smith.atlassian.com 24 | resources: 25 | - bundles 26 | verbs: 27 | - list 28 | - watch 29 | - update # need to be able to update finalizers 30 | 31 | - apiGroups: 32 | - smith.atlassian.com 33 | resources: 34 | - bundles/status 35 | verbs: 36 | - update 37 | 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - configmaps 42 | - secrets 43 | - services 44 | verbs: 45 | - list 46 | - watch 47 | - create 48 | - update 49 | - delete 50 | 51 | - apiGroups: 52 | - apps 53 | resources: 54 | - deployments 55 | verbs: 56 | - list 57 | - watch 58 | - create 59 | - update 60 | - delete 61 | 62 | - apiGroups: 63 | - extensions 64 | resources: 65 | - ingresses 66 | verbs: 67 | - list 68 | - watch 69 | - create 70 | - update 71 | - delete 72 | 73 | - apiGroups: 74 | - autoscaling 75 | resources: 76 | - horizontalpodautoscalers 77 | verbs: 78 | - list 79 | - watch 80 | - create 81 | - update 82 | - delete 83 | 84 | - apiGroups: 85 | - servicecatalog.k8s.io 86 | resources: 87 | - servicebindings 88 | - serviceinstances 89 | verbs: 90 | - list 91 | - watch 92 | - create 93 | - update 94 | - delete 95 | --- 96 | apiVersion: rbac.authorization.k8s.io/v1 97 | kind: ClusterRoleBinding 98 | metadata: 99 | name: smith-cluster-binding 100 | roleRef: 101 | apiGroup: rbac.authorization.k8s.io 102 | kind: ClusterRole 103 | name: smith-cluster-role 104 | subjects: 105 | - kind: ServiceAccount 106 | name: smith 107 | namespace: smith 108 | --- 109 | apiVersion: rbac.authorization.k8s.io/v1 110 | kind: RoleBinding 111 | metadata: 112 | name: smith-namespaced-binding 113 | namespace: 114 | roleRef: 115 | apiGroup: rbac.authorization.k8s.io 116 | kind: ClusterRole 117 | name: smith-namespaced-role 118 | subjects: 119 | - kind: ServiceAccount 120 | name: smith 121 | namespace: smith 122 | -------------------------------------------------------------------------------- /docs/deployment/3-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: smith 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: smith 10 | strategy: 11 | type: RollingUpdate 12 | rollingUpdate: 13 | maxSurge: 0 14 | maxUnavailable: 1 15 | template: 16 | metadata: 17 | labels: 18 | app: smith 19 | spec: 20 | serviceAccountName: smith 21 | containers: 22 | - name: smith 23 | image: "atlassianlabs/smith:0.0.X-XXXX" 24 | # args: 25 | # - '-namespace' 26 | # - "" 27 | -------------------------------------------------------------------------------- /docs/design/authorization.md: -------------------------------------------------------------------------------- 1 | # Authorization 2 | 3 | ## Problem statement 4 | 5 | Smith typically has a very broad set of permissions because it needs to be able to manage objects of various kinds 6 | across several/many namespaces in a cluster. If permissions checks are not done properly, a malicious user may have 7 | a way to escalate their privileges. 8 | This document describes a way to address the problem of blocking ways to permissions escalation. 9 | 10 | ## Proposed solution 11 | 12 | The idea is to capture identity of the user who creates/updates a `Bundle` and impersonate them when working with 13 | objects of that `Bundle`. That way Smith becomes just a tool that can automate only the operations that the user 14 | can perform already, it does not allow the user to do something that they are not allowed to do. 15 | 16 | Implementation is straightforward. 17 | 18 | 1. Use a [MutatingAdmissionWebhook](https://kubernetes.io/docs/admin/admission-controllers/#mutatingadmissionwebhook-beta-in-19) 19 | to capture information about the user's identity as a field in the `Bundle`. 20 | 2. [Impersonate the user](https://kubernetes.io/docs/admin/authentication/#user-impersonation) when making any requests 21 | related to the `Bundle`. This includes reads from informers' caches/indexes. 22 | -------------------------------------------------------------------------------- /docs/design/object-references.md: -------------------------------------------------------------------------------- 1 | # External object references 2 | 3 | An external object reference can be used to reference an object that is not part of the Bundle being processed. 4 | 5 | ## Motivation 6 | 7 | When using [plugins](plugins.md), sometimes it is necessary to invoke the plugin with an object that is not 8 | part of the Bundle being processed. A reference can be used to make Smith fetch an external object and 9 | pass it to the plugin. 10 | 11 | ## Specification 12 | 13 | References can only reference objects inside of the same namespace (namespaced references) or 14 | non-namespaced objects (cluster references). 15 | 16 | ### Namespaced references 17 | 18 | To reference an object in the same namespace, its name, group, version and kind are needed. Example: 19 | 20 | ```yaml 21 | apiVersion: smith.atlassian.com/v1 22 | kind: Bundle 23 | metadata: 24 | name: b1 25 | namespace: namespace123 26 | spec: 27 | resources: 28 | 29 | - name: extra-ref 30 | spec: 31 | reference: 32 | apiVersion: rbac.authorization.k8s.io/v1 33 | kind: RoleBinding 34 | name: binding1 35 | ``` 36 | 37 | ### Cluster references 38 | 39 | To reference a non-namespaced object, its name, group, version and kind are needed. Example: 40 | 41 | ```yaml 42 | apiVersion: smith.atlassian.com/v1 43 | kind: Bundle 44 | metadata: 45 | name: b2 46 | namespace: namespace123 47 | spec: 48 | resources: 49 | 50 | - name: cluster-extra-ref 51 | spec: 52 | clusterReference: 53 | apiVersion: rbac.authorization.k8s.io/v1 54 | kind: ClusterRoleBinding 55 | name: binding2 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/design/soft-deletes.md: -------------------------------------------------------------------------------- 1 | # Soft deletes 2 | 3 | ## Problem statement 4 | 5 | In a declarative system lack of description of a resource means that it should not exist. So if it does, 6 | it should be deleted. This is a potentially dangerous action that needs some safety around it to prevent irreversible 7 | consequences deletion may have. 8 | 9 | If resource is removed from a Bundle due to user error or a bug, it will be deleted once the Bundle reaches 10 | the "ready" state. To reduce impact of such mistake it is beneficial to have some sort of "soft deletion" mechanism. 11 | 12 | ## Solution 13 | 14 | Soft deletion is an opt-in feature based on resource definition in the Bundle enabled 15 | via a special annotation `smith.a.c/deletionDelay`. This annotation will provide 16 | a delay that the object should be kept in Kubernetes until proceeding with deletion. 17 | 18 | When Smith detects an object in Kubernetes that is owned by a Bundle but is 19 | missing from the resource list in the Bundle spec, it will check whether the object 20 | is marked with the `smith.a.c/deletionDelay` annotation. The annotation value is 21 | a delay in the [Go duration format](https://golang.org/pkg/time/#ParseDuration). 22 | 23 | If this annotation is present, Smith will annotate the corresponding object with 24 | `smith.a.c/deletionTimestamp` with the current timestamp (`time.Now()`) as a value 25 | to initiate the "countdown" of the delay, instead of issuing a delete immediately. 26 | Then it will periodically keep checking if the deletion delay has expired. 27 | Once it does, Smith will issue a delete request to Kubernetes API, thus **triggering 28 | the actual deletion** of an object (this is a potentially irreversible action, 29 | or "hard delete", so the deletion delay should be long enough for the user to be 30 | able to cancel it, see below). 31 | 32 | If the resource appears in the Bundle before the deletion delay has expired, Smith 33 | will detect an existing "orphaned" resource, and will "adopt" it and remove the 34 | `smith.a.c/deletionTimestamp` annotation, thus cancelling the deletion countdown. 35 | It will also continue updating "actual" object spec to make it match the "desired" 36 | spec declared in the Bundle. In other words, the normal processing of the resource 37 | will continue without actual deletion. 38 | 39 | ## Example 40 | 41 | ```yaml 42 | apiVersion: smith.atlassian.com/v1 43 | kind: Bundle 44 | metadata: 45 | name: my-bundle 46 | namespace: my-ns 47 | spec: 48 | resources: 49 | - name: my-db 50 | spec: 51 | object: 52 | apiVersion: servicecatalog.k8s.io/v1beta1 53 | kind: ServiceInstance 54 | metadata: 55 | name: db 56 | annotations: 57 | # 24-hour delay before proceeding with deletion 58 | smith.atlassian.com/deletionDelay: 24h 59 | spec: 60 | clusterServiceClassExternalID: 59c9355a-92d4-4c07-a633-96f6fa51abf1 61 | clusterServicePlanExternalID: 7b002afa-4197-4a50-a41a-df2dec4b5cfa 62 | parameters: 63 | ... 64 | ``` 65 | 66 | ## Forced "hard delete" 67 | 68 | User may want to force the "hard delete" (i.e. the actual deletion of Kubernetes object) 69 | instead of waiting for the deletion delay to expire. To do that, currently user needs to 70 | manually issue a delete request of the underlying Kubernetes object, e.g. using a 71 | corresponding `kubectl delete` command. 72 | -------------------------------------------------------------------------------- /examples/service_catalog/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: smith.atlassian.com/v1 2 | kind: Bundle 3 | metadata: 4 | name: sampleapp 5 | spec: 6 | resources: 7 | 8 | - name: instance1 9 | spec: 10 | apiVersion: servicecatalog.k8s.io/v1beta1 11 | kind: ServiceInstance 12 | metadata: 13 | name: instance1 14 | spec: 15 | serviceClassName: user-provided-service 16 | planName: default 17 | parameters: 18 | credentials: 19 | token: token 20 | 21 | - name: binding1 22 | references: 23 | - name: instance1-metadata-name 24 | resource: instance1 25 | path: metadata.name 26 | spec: 27 | apiVersion: servicecatalog.k8s.io/v1beta1 28 | kind: ServiceBinding 29 | metadata: 30 | name: binding1 31 | spec: 32 | instanceRef: 33 | name: "!{instance1-metadata-name}" 34 | secretName: secret1 35 | 36 | - name: binding2 37 | references: 38 | - name: instance1-metadata-name 39 | resource: instance1 40 | path: metadata.name 41 | spec: 42 | apiVersion: servicecatalog.k8s.io/v1beta1 43 | kind: ServiceBinding 44 | metadata: 45 | name: binding2 46 | spec: 47 | instanceRef: 48 | name: "!{instance1-metadata-name}" 49 | secretName: secret2 50 | 51 | - name: podpreset1 52 | references: 53 | - name: binding1-secretName 54 | resource: binding1 55 | path: spec.secretName 56 | - name: binding2-secretName 57 | resource: binding2 58 | path: spec.secretName 59 | spec: 60 | apiVersion: settings.k8s.io/v1alpha1 61 | kind: PodPreset 62 | metadata: 63 | name: podpreset1 64 | spec: 65 | selector: 66 | matchLabels: 67 | role: app 68 | envFrom: 69 | - prefix: BINDING1_ 70 | secretRef: 71 | name: "!{binding1-secretName}" 72 | - prefix: BINDING2_ 73 | secretRef: 74 | name: "!{binding2-secretName}" 75 | 76 | - name: deployment1 77 | references: 78 | - name: podpreset1-matchLabels 79 | resource: podpreset1 80 | path: spec.selector.matchLabels 81 | spec: 82 | apiVersion: apps/v1beta2 83 | kind: Deployment 84 | metadata: 85 | name: deployment1 86 | spec: 87 | replicas: 2 88 | template: 89 | metadata: 90 | labels: "!{podpreset1-matchLabels}" 91 | spec: 92 | containers: 93 | - name: nginx 94 | image: nginx:latest 95 | ports: 96 | - containerPort: 80 97 | 98 | - name: service1 99 | references: 100 | - name: deployment1-labels 101 | resource: deployment1 102 | path: spec.template.metadata.labels 103 | spec: 104 | apiVersion: v1 105 | kind: Service 106 | metadata: 107 | name: service1 108 | spec: 109 | ports: 110 | - port: 80 111 | protocol: TCP 112 | targetPort: 80 113 | nodePort: 30090 114 | selector: "!{deployment1-labels}" 115 | type: NodePort 116 | 117 | - name: ingress1 118 | references: 119 | - name: service1-metadata-name 120 | resource: service1 121 | path: metadata.name 122 | - name: service1-port 123 | resource: service1 124 | path: spec.ports[?(@.protocol=="TCP")].port 125 | spec: 126 | apiVersion: extensions/v1beta1 127 | kind: Ingress 128 | metadata: 129 | name: ingress1 130 | spec: 131 | rules: 132 | - http: 133 | paths: 134 | - path: / 135 | backend: 136 | serviceName: "!{service1-metadata-name}" 137 | servicePort: "!{service1-port}" 138 | -------------------------------------------------------------------------------- /examples/service_catalog/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | kubectl delete bundles/sampleapp 4 | kubectl delete podpresets/podpreset1 5 | kubectl delete deployments/deployment1 6 | kubectl delete service/service1 7 | kubectl delete ingress/ingress1 8 | kubectl delete secrets/secret1 9 | kubectl delete secrets/secret2 10 | kubectl delete serviceinstance/instance1 11 | kubectl delete servicebinding/binding1 12 | kubectl delete servicebinding/binding2 13 | -------------------------------------------------------------------------------- /examples/service_catalog/img/Application_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/smith/f1b74e89ebe44e485df1f5d33e0e98868fcae1a2/examples/service_catalog/img/Application_example.png -------------------------------------------------------------------------------- /examples/service_catalog/img/Kubernetes_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/smith/f1b74e89ebe44e485df1f5d33e0e98868fcae1a2/examples/service_catalog/img/Kubernetes_graph.png -------------------------------------------------------------------------------- /examples/service_catalog/img/asciinema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/smith/f1b74e89ebe44e485df1f5d33e0e98868fcae1a2/examples/service_catalog/img/asciinema.png -------------------------------------------------------------------------------- /examples/sleeper/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "app.go", 7 | "client.go", 8 | "sleeper_event_handler.go", 9 | ], 10 | importpath = "github.com/atlassian/smith/examples/sleeper", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//:go_default_library", 14 | "//examples/sleeper/pkg/apis/sleeper/v1:go_default_library", 15 | "//vendor/github.com/atlassian/ctrl/logz:go_default_library", 16 | "//vendor/go.uber.org/zap:go_default_library", 17 | "//vendor/k8s.io/api/core/v1:go_default_library", 18 | "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", 19 | "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", 20 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 21 | "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", 22 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 23 | "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", 24 | "//vendor/k8s.io/client-go/rest:go_default_library", 25 | "//vendor/k8s.io/client-go/tools/cache:go_default_library", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /examples/sleeper/app.go: -------------------------------------------------------------------------------- 1 | package sleeper 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | sleeper_v1 "github.com/atlassian/smith/examples/sleeper/pkg/apis/sleeper/v1" 8 | "go.uber.org/zap" 9 | "k8s.io/apimachinery/pkg/fields" 10 | "k8s.io/client-go/rest" 11 | "k8s.io/client-go/tools/cache" 12 | ) 13 | 14 | const ( 15 | ResyncPeriod = 20 * time.Minute 16 | ) 17 | 18 | type App struct { 19 | Logger *zap.Logger 20 | RestConfig *rest.Config 21 | Namespace string 22 | } 23 | 24 | func (a *App) Run(ctx context.Context) error { 25 | sClient, err := Client(a.RestConfig) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | // Create an Informer for Sleeper objects 31 | sleeperInformer := a.sleeperInformer(ctx, sClient) 32 | 33 | // Run until a signal to stop 34 | sleeperInformer.Run(ctx.Done()) 35 | return ctx.Err() 36 | } 37 | 38 | func (a *App) sleeperInformer(ctx context.Context, sClient rest.Interface) cache.SharedInformer { 39 | sleeperInf := cache.NewSharedInformer( 40 | cache.NewListWatchFromClient(sClient, sleeper_v1.SleeperResourcePlural, a.Namespace, fields.Everything()), 41 | &sleeper_v1.Sleeper{}, ResyncPeriod) 42 | 43 | eh := &EventHandler{ 44 | ctx: ctx, 45 | logger: a.Logger, 46 | client: sClient, 47 | } 48 | 49 | sleeperInf.AddEventHandler(eh) 50 | 51 | return sleeperInf 52 | } 53 | -------------------------------------------------------------------------------- /examples/sleeper/client.go: -------------------------------------------------------------------------------- 1 | package sleeper 2 | 3 | import ( 4 | "github.com/atlassian/smith" 5 | sleeper_v1 "github.com/atlassian/smith/examples/sleeper/pkg/apis/sleeper/v1" 6 | core_v1 "k8s.io/api/core/v1" 7 | apiext_v1b1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 8 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/serializer" 11 | "k8s.io/client-go/rest" 12 | ) 13 | 14 | func Scheme() (*runtime.Scheme, error) { 15 | scheme := runtime.NewScheme() 16 | scheme.AddUnversionedTypes(core_v1.SchemeGroupVersion, &meta_v1.Status{}) 17 | if err := sleeper_v1.AddToScheme(scheme); err != nil { 18 | return nil, err 19 | } 20 | return scheme, nil 21 | } 22 | 23 | func Client(cfg *rest.Config) (*rest.RESTClient, error) { 24 | scheme, err := Scheme() 25 | if err != nil { 26 | return nil, err 27 | } 28 | config := *cfg 29 | config.GroupVersion = &sleeper_v1.SchemeGroupVersion 30 | config.APIPath = "/apis" 31 | config.ContentType = runtime.ContentTypeJSON 32 | config.NegotiatedSerializer = serializer.NewCodecFactory(scheme).WithoutConversion() 33 | 34 | return rest.RESTClientFor(&config) 35 | } 36 | 37 | func Crd() *apiext_v1b1.CustomResourceDefinition { 38 | return &apiext_v1b1.CustomResourceDefinition{ 39 | ObjectMeta: meta_v1.ObjectMeta{ 40 | Name: sleeper_v1.SleeperResourceName, 41 | Annotations: map[string]string{ 42 | smith.CrFieldPathAnnotation: sleeper_v1.SleeperReadyStatePath, 43 | smith.CrFieldValueAnnotation: string(sleeper_v1.SleeperReadyStateValue), 44 | smith.CrdSupportEnabled: "true", 45 | }, 46 | }, 47 | Spec: apiext_v1b1.CustomResourceDefinitionSpec{ 48 | Group: sleeper_v1.GroupName, 49 | Names: apiext_v1b1.CustomResourceDefinitionNames{ 50 | Plural: sleeper_v1.SleeperResourcePlural, 51 | Singular: sleeper_v1.SleeperResourceSingular, 52 | Kind: sleeper_v1.SleeperResourceKind, 53 | }, 54 | Versions: []apiext_v1b1.CustomResourceDefinitionVersion{ 55 | { 56 | Name: sleeper_v1.SleeperResourceVersion, 57 | Served: true, 58 | Storage: true, 59 | }, 60 | }, 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/sleeper/main/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["main.go"], 6 | importpath = "github.com/atlassian/smith/examples/sleeper/main", 7 | visibility = ["//visibility:private"], 8 | deps = [ 9 | "//examples/sleeper:go_default_library", 10 | "//vendor/github.com/atlassian/ctrl/app:go_default_library", 11 | "//vendor/github.com/atlassian/ctrl/options:go_default_library", 12 | "//vendor/k8s.io/client-go/rest:go_default_library", 13 | ], 14 | ) 15 | 16 | go_binary( 17 | name = "main", 18 | embed = [":go_default_library"], 19 | pure = "on", 20 | tags = ["manual"], 21 | visibility = ["//visibility:public"], 22 | ) 23 | 24 | go_binary( 25 | name = "main_race", 26 | embed = [":go_default_library"], 27 | race = "on", 28 | visibility = ["//visibility:public"], 29 | ) 30 | -------------------------------------------------------------------------------- /examples/sleeper/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | ctrlApp "github.com/atlassian/ctrl/app" 10 | "github.com/atlassian/ctrl/options" 11 | "github.com/atlassian/smith/examples/sleeper" 12 | "k8s.io/client-go/rest" 13 | ) 14 | 15 | func main() { 16 | if err := run(); err != nil && err != context.Canceled && err != context.DeadlineExceeded { 17 | fmt.Fprintf(os.Stderr, "%#v", err) // nolint: gas, errcheck 18 | os.Exit(1) 19 | } 20 | } 21 | 22 | func run() error { 23 | ctx, cancelFunc := context.WithCancel(context.Background()) 24 | defer cancelFunc() 25 | ctrlApp.CancelOnInterrupt(ctx, cancelFunc) 26 | 27 | return runWithContext(ctx) 28 | } 29 | 30 | func runWithContext(ctx context.Context) error { 31 | var restClientOpts options.RestClientOptions 32 | options.BindRestClientFlags(&restClientOpts, flag.CommandLine) 33 | 34 | flag.Parse() 35 | 36 | config, err := options.LoadRestClientConfig("sleeper-controller", restClientOpts) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return runWithConfig(ctx, config) 42 | } 43 | 44 | func runWithConfig(ctx context.Context, config *rest.Config) error { 45 | a := sleeper.App{ 46 | Logger: options.LoggerFromOptions(options.LoggerOptions{ 47 | LogLevel: "debug", 48 | LogEncoding: "console", 49 | }), 50 | RestConfig: config, 51 | } 52 | return a.Run(ctx) 53 | } 54 | -------------------------------------------------------------------------------- /examples/sleeper/pkg/apis/sleeper/v1/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "register.go", 7 | "types.go", 8 | "zz_generated.deepcopy.go", 9 | ], 10 | importpath = "github.com/atlassian/smith/examples/sleeper/pkg/apis/sleeper/v1", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 14 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 15 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 16 | ], 17 | ) 18 | 19 | go_test( 20 | name = "go_default_test", 21 | size = "small", 22 | srcs = ["types_test.go"], 23 | embed = [":go_default_library"], 24 | race = "on", 25 | deps = [ 26 | "//pkg/resources:go_default_library", 27 | "//vendor/github.com/stretchr/testify/assert:go_default_library", 28 | "//vendor/github.com/stretchr/testify/require:go_default_library", 29 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 30 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 31 | "//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /examples/sleeper/pkg/apis/sleeper/v1/register.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | // GroupName is the group name use in this package. 10 | const GroupName = CrdDomain 11 | 12 | // SchemeGroupVersion is group version used to register these objects. 13 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} 14 | 15 | // Kind takes an unqualified kind and returns a Group qualified GroupKind. 16 | func Kind(kind string) schema.GroupKind { 17 | return SchemeGroupVersion.WithKind(kind).GroupKind() 18 | } 19 | 20 | var ( 21 | // SchemeBuilder needs to be exported as `SchemeBuilder` so 22 | // the code-generation can find it. 23 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 24 | // AddToScheme is exposed for API installation 25 | AddToScheme = SchemeBuilder.AddToScheme 26 | ) 27 | 28 | func addKnownTypes(scheme *runtime.Scheme) error { 29 | scheme.AddKnownTypes(SchemeGroupVersion, 30 | &Sleeper{}, 31 | &SleeperList{}, 32 | ) 33 | meta_v1.AddToGroupVersion(scheme, SchemeGroupVersion) 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /examples/sleeper/pkg/apis/sleeper/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | const ( 8 | CrdDomain = "crd.atlassian.com" 9 | 10 | SleeperResourceSingular = "sleeper" 11 | SleeperResourcePlural = "sleepers" 12 | SleeperResourceVersion = "v1" 13 | SleeperResourceKind = "Sleeper" 14 | 15 | SleeperResourceGroupVersion = GroupName + "/" + SleeperResourceVersion 16 | 17 | SleeperResourceName = SleeperResourcePlural + "." + CrdDomain 18 | 19 | SleeperReadyStatePath = "{$.status.state}" 20 | SleeperReadyStateValue = Awake 21 | ) 22 | 23 | type SleeperState string 24 | 25 | const ( 26 | New SleeperState = "" 27 | Sleeping SleeperState = "Sleeping" 28 | Awake SleeperState = "Awake!" 29 | Error SleeperState = "Error" 30 | ) 31 | 32 | var SleeperGVK = SchemeGroupVersion.WithKind(SleeperResourceKind) 33 | 34 | // +k8s:deepcopy-gen=true 35 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 36 | type SleeperList struct { 37 | meta_v1.TypeMeta `json:",inline"` 38 | // Standard list metadata. 39 | meta_v1.ListMeta `json:"metadata,omitempty"` 40 | 41 | // Items is a list of sleepers. 42 | Items []Sleeper `json:"items"` 43 | } 44 | 45 | // +k8s:deepcopy-gen=true 46 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 47 | // Sleeper describes a sleeping resource. 48 | type Sleeper struct { 49 | meta_v1.TypeMeta `json:",inline"` 50 | 51 | // Standard object metadata 52 | meta_v1.ObjectMeta `json:"metadata,omitempty"` 53 | 54 | // Spec is the specification of the desired behavior of the Sleeper. 55 | Spec SleeperSpec `json:"spec,omitempty"` 56 | 57 | // Status is most recently observed status of the Sleeper. 58 | Status SleeperStatus `json:"status,omitempty"` 59 | } 60 | 61 | // +k8s:deepcopy-gen=true 62 | type SleeperSpec struct { 63 | SleepFor int `json:"sleepFor"` 64 | WakeupMessage string `json:"wakeupMessage"` 65 | } 66 | 67 | // +k8s:deepcopy-gen=true 68 | type SleeperStatus struct { 69 | State SleeperState `json:"state,omitempty"` 70 | Message string `json:"message,omitempty"` 71 | } 72 | -------------------------------------------------------------------------------- /examples/sleeper/pkg/apis/sleeper/v1/types_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/atlassian/smith/pkg/resources" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/util/json" 12 | ) 13 | 14 | var _ runtime.Object = &SleeperList{} 15 | var _ meta_v1.ListMetaAccessor = &SleeperList{} 16 | 17 | var _ runtime.Object = &Sleeper{} 18 | var _ meta_v1.ObjectMetaAccessor = &Sleeper{} 19 | 20 | func TestGetJsonPathStringSleeper(t *testing.T) { 21 | t.Parallel() 22 | b := &Sleeper{ 23 | Status: SleeperStatus{ 24 | State: Awake, 25 | }, 26 | } 27 | bytes, err := json.Marshal(b) 28 | require.NoError(t, err) 29 | unstructured := make(map[string]interface{}) 30 | err = json.Unmarshal(bytes, &unstructured) 31 | require.NoError(t, err) 32 | status, err := resources.GetJSONPathString(unstructured, SleeperReadyStatePath) 33 | require.NoError(t, err) 34 | assert.Equal(t, string(SleeperReadyStateValue), status) 35 | } 36 | -------------------------------------------------------------------------------- /examples/sleeper/pkg/apis/sleeper/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Generated file, do not modify manually! 4 | 5 | // Code generated by deepcopy-gen. DO NOT EDIT. 6 | 7 | package v1 8 | 9 | import ( 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 14 | func (in *Sleeper) DeepCopyInto(out *Sleeper) { 15 | *out = *in 16 | out.TypeMeta = in.TypeMeta 17 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 18 | out.Spec = in.Spec 19 | out.Status = in.Status 20 | return 21 | } 22 | 23 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sleeper. 24 | func (in *Sleeper) DeepCopy() *Sleeper { 25 | if in == nil { 26 | return nil 27 | } 28 | out := new(Sleeper) 29 | in.DeepCopyInto(out) 30 | return out 31 | } 32 | 33 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 34 | func (in *Sleeper) DeepCopyObject() runtime.Object { 35 | if c := in.DeepCopy(); c != nil { 36 | return c 37 | } 38 | return nil 39 | } 40 | 41 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 42 | func (in *SleeperList) DeepCopyInto(out *SleeperList) { 43 | *out = *in 44 | out.TypeMeta = in.TypeMeta 45 | in.ListMeta.DeepCopyInto(&out.ListMeta) 46 | if in.Items != nil { 47 | in, out := &in.Items, &out.Items 48 | *out = make([]Sleeper, len(*in)) 49 | for i := range *in { 50 | (*in)[i].DeepCopyInto(&(*out)[i]) 51 | } 52 | } 53 | return 54 | } 55 | 56 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SleeperList. 57 | func (in *SleeperList) DeepCopy() *SleeperList { 58 | if in == nil { 59 | return nil 60 | } 61 | out := new(SleeperList) 62 | in.DeepCopyInto(out) 63 | return out 64 | } 65 | 66 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 67 | func (in *SleeperList) DeepCopyObject() runtime.Object { 68 | if c := in.DeepCopy(); c != nil { 69 | return c 70 | } 71 | return nil 72 | } 73 | 74 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 75 | func (in *SleeperSpec) DeepCopyInto(out *SleeperSpec) { 76 | *out = *in 77 | return 78 | } 79 | 80 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SleeperSpec. 81 | func (in *SleeperSpec) DeepCopy() *SleeperSpec { 82 | if in == nil { 83 | return nil 84 | } 85 | out := new(SleeperSpec) 86 | in.DeepCopyInto(out) 87 | return out 88 | } 89 | 90 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 91 | func (in *SleeperStatus) DeepCopyInto(out *SleeperStatus) { 92 | *out = *in 93 | return 94 | } 95 | 96 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SleeperStatus. 97 | func (in *SleeperStatus) DeepCopy() *SleeperStatus { 98 | if in == nil { 99 | return nil 100 | } 101 | out := new(SleeperStatus) 102 | in.DeepCopyInto(out) 103 | return out 104 | } 105 | -------------------------------------------------------------------------------- /examples/sleeper/sleeper_event_handler.go: -------------------------------------------------------------------------------- 1 | package sleeper 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | ctrlLogz "github.com/atlassian/ctrl/logz" 8 | sleeper_v1 "github.com/atlassian/smith/examples/sleeper/pkg/apis/sleeper/v1" 9 | "go.uber.org/zap" 10 | api_errors "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/client-go/rest" 12 | ) 13 | 14 | type EventHandler struct { 15 | ctx context.Context 16 | logger *zap.Logger 17 | client rest.Interface 18 | } 19 | 20 | func (h *EventHandler) OnAdd(obj interface{}) { 21 | h.handle(obj) 22 | } 23 | 24 | func (h *EventHandler) OnUpdate(oldObj, newObj interface{}) { 25 | in := *newObj.(*sleeper_v1.Sleeper) 26 | if in.Status.State == sleeper_v1.New { 27 | h.handle(newObj) 28 | } 29 | } 30 | 31 | func (h *EventHandler) OnDelete(obj interface{}) { 32 | } 33 | 34 | func (h *EventHandler) handle(obj interface{}) { 35 | in := obj.(*sleeper_v1.Sleeper).DeepCopy() 36 | msg := in.Spec.WakeupMessage 37 | logger := h.logger.With(ctrlLogz.Namespace(in), ctrlLogz.Object(in)) 38 | logger.Sugar().Infof("Sleeper was added/updated. Setting Status to %q and falling asleep for %d seconds... ZZzzzz", sleeper_v1.Sleeping, in.Spec.SleepFor) 39 | err := h.retryUpdate(in, sleeper_v1.Sleeping, "") 40 | if err != nil { 41 | logger.Error("Status update failed", zap.Error(err)) 42 | return 43 | } 44 | go func() { 45 | select { 46 | case <-h.ctx.Done(): 47 | return 48 | case <-time.After(time.Duration(in.Spec.SleepFor) * time.Second): 49 | logger.Sugar().Infof("%s Updating Sleeper Status to %q", in.Spec.WakeupMessage, sleeper_v1.Awake) 50 | err = h.retryUpdate(in, sleeper_v1.Awake, msg) 51 | if err != nil { 52 | logger.Error("Status update failed", zap.Error(err)) 53 | } 54 | } 55 | }() 56 | } 57 | 58 | func (h *EventHandler) retryUpdate(sleeper *sleeper_v1.Sleeper, state sleeper_v1.SleeperState, message string) error { 59 | for { 60 | sleeper.Status.State = state 61 | sleeper.Status.Message = message 62 | err := h.client.Put(). 63 | Context(h.ctx). 64 | Namespace(sleeper.Namespace). 65 | Resource(sleeper_v1.SleeperResourcePlural). 66 | Name(sleeper.Name). 67 | Body(sleeper). 68 | Do(). 69 | Into(sleeper) 70 | if api_errors.IsConflict(err) { 71 | err = h.client.Get(). 72 | Context(h.ctx). 73 | Namespace(sleeper.Namespace). 74 | Resource(sleeper_v1.SleeperResourcePlural). 75 | Name(sleeper.Name). 76 | Do(). 77 | Into(sleeper) 78 | if err != nil { 79 | return err 80 | } 81 | continue 82 | } 83 | return err 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/atlassian/smith 2 | 3 | go 1.13.1 4 | 5 | require ( 6 | github.com/ash2k/stager v0.0.0-20170622123058-6e9c7b0eacd4 7 | github.com/atlassian/ctrl v0.0.0-20190816021437-9632032e4bf6 8 | github.com/evanphx/json-patch v4.5.0+incompatible // indirect 9 | github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 // indirect 10 | github.com/googleapis/gnostic v0.3.1 // indirect 11 | github.com/imdario/mergo v0.3.6 // indirect 12 | github.com/kubernetes-sigs/service-catalog v0.3.0-beta.1 13 | github.com/pkg/errors v0.8.1 14 | github.com/prometheus/client_golang v0.9.4 15 | github.com/stretchr/testify v1.3.0 16 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 17 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 18 | github.com/xeipuuv/gojsonschema v1.1.0 19 | go.uber.org/zap v1.10.0 20 | gopkg.in/inf.v0 v0.9.1 // indirect 21 | gopkg.in/yaml.v2 v2.2.4 // indirect 22 | k8s.io/api v0.0.0-20191003000013-35e20aa79eb8 23 | k8s.io/apiextensions-apiserver v0.0.0-20191003002041-49e3d608220c 24 | k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 25 | k8s.io/client-go v0.0.0-20191003000419-f68efa97b39e 26 | k8s.io/code-generator v0.0.0-20190927045949-f81bca4f5e85 27 | k8s.io/klog v1.0.0 // indirect 28 | k8s.io/utils v0.0.0-20190920012459-5008bf6f8cd6 // indirect 29 | sigs.k8s.io/controller-runtime v0.2.0-alpha.0 // indirect 30 | sigs.k8s.io/yaml v1.1.0 31 | ) 32 | -------------------------------------------------------------------------------- /it/crd_attribute_test.go: -------------------------------------------------------------------------------- 1 | package it 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/ash2k/stager" 9 | "github.com/atlassian/smith/examples/sleeper" 10 | sleeper_v1 "github.com/atlassian/smith/examples/sleeper/pkg/apis/sleeper/v1" 11 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | ) 16 | 17 | func TestCrdAttribute(t *testing.T) { 18 | t.Parallel() 19 | sl := &sleeper_v1.Sleeper{ 20 | TypeMeta: meta_v1.TypeMeta{ 21 | Kind: sleeper_v1.SleeperResourceKind, 22 | APIVersion: sleeper_v1.SleeperResourceGroupVersion, 23 | }, 24 | ObjectMeta: meta_v1.ObjectMeta{ 25 | Name: "sleeper1", 26 | }, 27 | Spec: sleeper_v1.SleeperSpec{ 28 | SleepFor: 1, // seconds, 29 | WakeupMessage: "Hello, Infravators!", 30 | }, 31 | } 32 | bundle := &smith_v1.Bundle{ 33 | TypeMeta: meta_v1.TypeMeta{ 34 | Kind: smith_v1.BundleResourceKind, 35 | APIVersion: smith_v1.BundleResourceGroupVersion, 36 | }, 37 | ObjectMeta: meta_v1.ObjectMeta{ 38 | Name: "bundle-attribute", 39 | }, 40 | Spec: smith_v1.BundleSpec{ 41 | Resources: []smith_v1.Resource{ 42 | { 43 | Name: smith_v1.ResourceName(sl.Name), 44 | Spec: smith_v1.ResourceSpec{ 45 | Object: sl, 46 | }, 47 | }, 48 | }, 49 | }, 50 | } 51 | SetupApp(t, bundle, false, true, testCrdAttribute, sl) 52 | } 53 | 54 | func testCrdAttribute(ctxTest context.Context, t *testing.T, cfg *Config, args ...interface{}) { 55 | sl := args[0].(*sleeper_v1.Sleeper) 56 | sClient, err := sleeper.Client(cfg.Config) 57 | require.NoError(t, err) 58 | 59 | stgr := stager.New() 60 | defer stgr.Shutdown() 61 | stage := stgr.NextStage() 62 | stage.StartWithContext(func(ctx context.Context) { 63 | apl := sleeper.App{ 64 | Logger: cfg.Logger, 65 | RestConfig: cfg.Config, 66 | Namespace: cfg.Namespace, 67 | } 68 | if e := apl.Run(ctx); e != context.Canceled && e != context.DeadlineExceeded { 69 | assert.NoError(t, e) 70 | } 71 | }) 72 | 73 | ctxTimeout, cancel := context.WithTimeout(ctxTest, time.Duration(sl.Spec.SleepFor+10)*time.Second) 74 | defer cancel() 75 | 76 | cfg.AssertBundle(ctxTimeout, cfg.Bundle) 77 | 78 | var sleeperObj sleeper_v1.Sleeper 79 | require.NoError(t, sClient.Get(). 80 | Context(ctxTest). 81 | Namespace(cfg.Namespace). 82 | Resource(sleeper_v1.SleeperResourcePlural). 83 | Name(sl.Name). 84 | Do(). 85 | Into(&sleeperObj)) 86 | 87 | assert.Empty(t, sleeperObj.Labels) 88 | assert.Equal(t, sleeper_v1.Awake, sleeperObj.Status.State) 89 | } 90 | -------------------------------------------------------------------------------- /it/main_test.go: -------------------------------------------------------------------------------- 1 | package it 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | rand.Seed(time.Now().UnixNano()) 12 | os.Exit(m.Run()) 13 | } 14 | -------------------------------------------------------------------------------- /it/sc/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_test") 2 | 3 | go_test( 4 | name = "go_default_test", 5 | size = "medium", 6 | timeout = "short", 7 | srcs = [ 8 | "instance_and_binding_depend_on_secret_test.go", 9 | "main_test.go", 10 | "service_catalog_test.go", 11 | "zz_objects_for_test.go", 12 | ], 13 | race = "on", 14 | tags = [ 15 | "external", 16 | "manual", 17 | ], 18 | deps = [ 19 | "//it:go_default_library", 20 | "//pkg/apis/smith/v1:go_default_library", 21 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1:go_default_library", 22 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/client/clientset_generated/clientset:go_default_library", 23 | "//vendor/github.com/stretchr/testify/require:go_default_library", 24 | "//vendor/k8s.io/api/core/v1:go_default_library", 25 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 26 | "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", 27 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 28 | "//vendor/k8s.io/client-go/tools/cache:go_default_library", 29 | "//vendor/k8s.io/client-go/tools/watch:go_default_library", 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /it/sc/instance_and_binding_depend_on_secret_test.go: -------------------------------------------------------------------------------- 1 | package sc 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/atlassian/smith/it" 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 10 | scClientset "github.com/kubernetes-sigs/service-catalog/pkg/client/clientset_generated/clientset" 11 | "github.com/stretchr/testify/require" 12 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/fields" 14 | "k8s.io/client-go/tools/cache" 15 | toolswatch "k8s.io/client-go/tools/watch" 16 | ) 17 | 18 | // Given: a Bundle, containing ServiceInstance and/or ServiceBinding. 19 | // Given: that ServiceInstance and/or ServiceBinding have parametersFrom block using a Secret 20 | // When: that Secret is created/updated/deleted 21 | // Then: Bundle should be enqueued for processing 22 | func TestSecretToBindingAndInstanceToBundleIndex(t *testing.T) { 23 | t.Parallel() 24 | bundle := &smith_v1.Bundle{ 25 | TypeMeta: meta_v1.TypeMeta{ 26 | Kind: smith_v1.BundleResourceKind, 27 | APIVersion: smith_v1.BundleResourceGroupVersion, 28 | }, 29 | ObjectMeta: meta_v1.ObjectMeta{ 30 | Name: "bundle-cs", 31 | }, 32 | Spec: smith_v1.BundleSpec{ 33 | Resources: []smith_v1.Resource{ 34 | { 35 | Name: smith_v1.ResourceName(serviceInstance1name), 36 | Spec: smith_v1.ResourceSpec{ 37 | Object: serviceInstance1withParametersFrom(), 38 | }, 39 | }, 40 | { 41 | Name: smith_v1.ResourceName(serviceBinding1name), 42 | References: []smith_v1.Reference{ 43 | { 44 | Resource: smith_v1.ResourceName(serviceInstance1name), 45 | }, 46 | }, 47 | Spec: smith_v1.ResourceSpec{ 48 | Object: serviceBinding1withParametersFrom(), 49 | }, 50 | }, 51 | }, 52 | }, 53 | } 54 | it.SetupApp(t, bundle, true, false, testSecretToBindingAndInstanceToBundleIndex) 55 | } 56 | 57 | func testSecretToBindingAndInstanceToBundleIndex(ctx context.Context, t *testing.T, cfg *it.Config, args ...interface{}) { 58 | scClient, err := scClientset.NewForConfig(cfg.Config) 59 | require.NoError(t, err) 60 | 61 | // initial create of Secret objects 62 | s1, err := cfg.MainClient.CoreV1().Secrets(cfg.Namespace).Create(secret1()) 63 | require.NoError(t, err) 64 | s2, err := cfg.MainClient.CoreV1().Secrets(cfg.Namespace).Create(secret2()) 65 | require.NoError(t, err) 66 | 67 | res := &smith_v1.Bundle{} 68 | cfg.CreateObject(ctx, cfg.Bundle, res, smith_v1.BundleResourcePlural, cfg.SmithClient.SmithV1().RESTClient()) 69 | cfg.CreatedBundle = res 70 | 71 | // Wait for stable state 72 | cfg.AssertBundle(ctx, cfg.Bundle) 73 | 74 | si, err := scClient.ServicecatalogV1beta1().ServiceInstances(cfg.Namespace).Get(serviceInstance1name, meta_v1.GetOptions{}) 75 | require.NoError(t, err) 76 | 77 | // Update Secret1 78 | s1.Data[secret1credentialsKey] = []byte(`{"token": "NEW token" }`) 79 | _, err = cfg.MainClient.CoreV1().Secrets(cfg.Namespace).Update(s1) 80 | require.NoError(t, err) 81 | 82 | // Wait for updated UpdateRequests 83 | lw := cache.NewListWatchFromClient(scClient.ServicecatalogV1beta1().RESTClient(), "serviceinstances", cfg.Namespace, fields.Everything()) 84 | cond := it.IsServiceInstanceUpdateRequestsCond(t, cfg.Namespace, si.Name, si.Spec.UpdateRequests) 85 | _, err = toolswatch.UntilWithSync(ctx, lw, &sc_v1b1.ServiceInstance{}, nil, cond) 86 | require.NoError(t, err) 87 | 88 | // Update Secret2 89 | s2.Data[secret1credentialsKey] = []byte(`{"token": "NEW token" }`) 90 | _, err = cfg.MainClient.CoreV1().Secrets(cfg.Namespace).Update(s2) 91 | require.NoError(t, err) 92 | 93 | // Nothing to wait for, ServiceBinding does not have a way to force an update. 94 | } 95 | -------------------------------------------------------------------------------- /it/sc/main_test.go: -------------------------------------------------------------------------------- 1 | package sc 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | rand.Seed(time.Now().UnixNano()) 12 | os.Exit(m.Run()) 13 | } 14 | -------------------------------------------------------------------------------- /it/sc/service_catalog_test.go: -------------------------------------------------------------------------------- 1 | package sc 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/atlassian/smith/it" 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | func TestServiceCatalog(t *testing.T) { 13 | t.Parallel() 14 | bundle := &smith_v1.Bundle{ 15 | TypeMeta: meta_v1.TypeMeta{ 16 | Kind: smith_v1.BundleResourceKind, 17 | APIVersion: smith_v1.BundleResourceGroupVersion, 18 | }, 19 | ObjectMeta: meta_v1.ObjectMeta{ 20 | Name: "bundle-cs", 21 | }, 22 | Spec: smith_v1.BundleSpec{ 23 | Resources: []smith_v1.Resource{ 24 | { 25 | Name: smith_v1.ResourceName(serviceInstance1name), 26 | Spec: smith_v1.ResourceSpec{ 27 | Object: serviceInstance1(), 28 | }, 29 | }, 30 | { 31 | Name: smith_v1.ResourceName(serviceBinding1name), 32 | References: []smith_v1.Reference{ 33 | { 34 | Resource: smith_v1.ResourceName(serviceInstance1name), 35 | }, 36 | }, 37 | Spec: smith_v1.ResourceSpec{ 38 | Object: serviceBinding1(), 39 | }, 40 | }, 41 | }, 42 | }, 43 | } 44 | it.SetupApp(t, bundle, true, true, testServiceCatalog) 45 | } 46 | 47 | func testServiceCatalog(ctx context.Context, t *testing.T, cfg *it.Config, args ...interface{}) { 48 | cfg.AssertBundle(ctx, cfg.Bundle) 49 | } 50 | -------------------------------------------------------------------------------- /it/sc/ups-clusterservicebroker.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: servicecatalog.k8s.io/v1beta1 2 | kind: ClusterServiceBroker 3 | metadata: 4 | name: ups-broker 5 | spec: 6 | url: http://ups-broker-ups-broker.ups-broker.svc.cluster.local 7 | -------------------------------------------------------------------------------- /it/workflow_test.go: -------------------------------------------------------------------------------- 1 | package it 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | core_v1 "k8s.io/api/core/v1" 11 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func TestWorkflow(t *testing.T) { 15 | t.Parallel() 16 | c1 := &core_v1.ConfigMap{ 17 | TypeMeta: meta_v1.TypeMeta{ 18 | Kind: "ConfigMap", 19 | APIVersion: "v1", 20 | }, 21 | ObjectMeta: meta_v1.ObjectMeta{ 22 | Name: "config1", 23 | Labels: map[string]string{ 24 | "configLabel": "configValue", 25 | "overlappingLabel": "overlappingConfigValue", 26 | }, 27 | }, 28 | Data: map[string]string{ 29 | "a": "b", 30 | }, 31 | } 32 | s1 := &core_v1.Secret{ 33 | TypeMeta: meta_v1.TypeMeta{ 34 | Kind: "Secret", 35 | APIVersion: "v1", 36 | }, 37 | ObjectMeta: meta_v1.ObjectMeta{ 38 | Name: "secret1", 39 | }, 40 | StringData: map[string]string{ 41 | "a": "b", 42 | }, 43 | } 44 | sa1 := &core_v1.ServiceAccount{ 45 | TypeMeta: meta_v1.TypeMeta{ 46 | Kind: "ServiceAccount", 47 | APIVersion: "v1", 48 | }, 49 | ObjectMeta: meta_v1.ObjectMeta{ 50 | Name: "sa1", 51 | }, 52 | } 53 | bundle := &smith_v1.Bundle{ 54 | TypeMeta: meta_v1.TypeMeta{ 55 | Kind: smith_v1.BundleResourceKind, 56 | APIVersion: smith_v1.BundleResourceGroupVersion, 57 | }, 58 | ObjectMeta: meta_v1.ObjectMeta{ 59 | Name: "bundle1", 60 | Labels: map[string]string{ 61 | "bundleLabel": "bundleValue", 62 | "overlappingLabel": "overlappingBundleValue", 63 | }, 64 | }, 65 | Spec: smith_v1.BundleSpec{ 66 | Resources: []smith_v1.Resource{ 67 | { 68 | Name: "config1res", 69 | References: []smith_v1.Reference{ 70 | {Resource: "secret2res"}, 71 | }, 72 | Spec: smith_v1.ResourceSpec{ 73 | Object: c1, 74 | }, 75 | }, 76 | { 77 | Name: "secret2res", 78 | Spec: smith_v1.ResourceSpec{ 79 | Object: s1, 80 | }, 81 | }, 82 | { 83 | Name: "sa1res", 84 | Spec: smith_v1.ResourceSpec{ 85 | Object: sa1, 86 | }, 87 | }, 88 | }, 89 | }, 90 | } 91 | SetupApp(t, bundle, false, true, testWorkflow) 92 | } 93 | 94 | func testWorkflow(ctx context.Context, t *testing.T, cfg *Config, args ...interface{}) { 95 | bundleRes := cfg.AssertBundleTimeout(ctx, cfg.Bundle) 96 | 97 | cfMap, err := cfg.MainClient.CoreV1().ConfigMaps(cfg.Namespace).Get("config1", meta_v1.GetOptions{}) 98 | require.NoError(t, err) 99 | 100 | secret, err := cfg.MainClient.CoreV1().Secrets(cfg.Namespace).Get("secret1", meta_v1.GetOptions{}) 101 | require.NoError(t, err) 102 | trueRef := true 103 | assert.Equal(t, []meta_v1.OwnerReference{ 104 | { 105 | APIVersion: smith_v1.BundleResourceGroupVersion, 106 | Kind: smith_v1.BundleResourceKind, 107 | Name: bundleRes.Name, 108 | UID: bundleRes.UID, 109 | Controller: &trueRef, 110 | BlockOwnerDeletion: &trueRef, 111 | }, 112 | { 113 | APIVersion: "v1", 114 | Kind: "Secret", 115 | Name: secret.Name, 116 | UID: secret.UID, 117 | BlockOwnerDeletion: &trueRef, 118 | }, 119 | }, cfMap.GetOwnerReferences()) 120 | assert.Equal(t, []meta_v1.OwnerReference{ 121 | { 122 | APIVersion: smith_v1.BundleResourceGroupVersion, 123 | Kind: smith_v1.BundleResourceKind, 124 | Name: bundleRes.Name, 125 | UID: bundleRes.UID, 126 | Controller: &trueRef, 127 | BlockOwnerDeletion: &trueRef, 128 | }, 129 | }, secret.GetOwnerReferences()) 130 | } 131 | -------------------------------------------------------------------------------- /misc/bundle_template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: smith.atlassian.com/v1 2 | kind: BundleTemplate 3 | description: "Service template stub" 4 | metadata: 5 | name: bundle_stub_1 6 | overridable: 7 | DEPLOYMENT_REPLICAS: 8 | default: 2 9 | schema: 10 | type: integer 11 | minimum: 1 12 | 13 | DEPLOYMENT_IMAGE: 14 | schema: 15 | type: string 16 | minLength: 1 17 | 18 | DEPLOYMENT_POD_SPEC: 19 | default: 20 | containers: 21 | - name: app 22 | image: "$(DEPLOYMENT_IMAGE)" 23 | ports: 24 | - containerPort: 8080 25 | schema: 26 | type: object 27 | 28 | spec: 29 | resources: 30 | - apiVersion: v1 31 | kind: ConfigMap 32 | metadata: 33 | name: config1 34 | data: 35 | a: b 36 | - apiVersion: extensions/v1beta1 37 | kind: Deployment 38 | metadata: 39 | name: app-deployment 40 | spec: 41 | replicas: "$((DEPLOYMENT_REPLICAS))" 42 | template: 43 | metadata: 44 | labels: 45 | app: app 46 | spec: "$((DEPLOYMENT_POD_SPEC))" 47 | strategy: 48 | type: RollingUpdate 49 | minReadySeconds: 0 50 | - apiVersion: extensions/v1beta1 51 | kind: Ingress 52 | metadata: 53 | name: app-ingress 54 | spec: 55 | -------------------------------------------------------------------------------- /misc/sleepers.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: smith.atlassian.com/v1 2 | kind: Bundle 3 | metadata: 4 | name: bundlex 5 | spec: 6 | resources: 7 | - name: sleeper1 8 | spec: 9 | apiVersion: crd.atlassian.com/v1 10 | kind: Sleeper 11 | metadata: 12 | name: sleeper1 13 | spec: 14 | sleepFor: 3 15 | wakeupMessage: Hello, Infravators! 16 | - name: sleeper2 17 | references: 18 | - name: sleeper1-status-message 19 | resource: sleeper1 20 | path: status.message 21 | spec: 22 | apiVersion: crd.atlassian.com/v1 23 | kind: Sleeper 24 | metadata: 25 | name: sleeper2 26 | spec: 27 | sleepFor: 4 28 | wakeupMessage: "!{sleeper1-status-message}" 29 | - name: sleeper3 30 | references: 31 | - name: sleeper2-spec 32 | resource: sleeper2 33 | path: spec 34 | spec: 35 | apiVersion: crd.atlassian.com/v1 36 | kind: Sleeper 37 | metadata: 38 | name: sleeper3 39 | spec: "!{sleeper2-spec}" 40 | -------------------------------------------------------------------------------- /pkg/apis/smith/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["register.go"], 6 | importpath = "github.com/atlassian/smith/pkg/apis/smith", 7 | visibility = ["//visibility:public"], 8 | deps = ["//:go_default_library"], 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/apis/smith/register.go: -------------------------------------------------------------------------------- 1 | package smith 2 | 3 | import ( 4 | "github.com/atlassian/smith" 5 | ) 6 | 7 | const ( 8 | // GroupName is the group name use in this package. 9 | GroupName = smith.Domain 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/apis/smith/v1/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "doc.go", 7 | "register.go", 8 | "types.go", 9 | "zz_generated.deepcopy.go", 10 | ], 11 | importpath = "github.com/atlassian/smith/pkg/apis/smith/v1", 12 | visibility = ["//visibility:public"], 13 | deps = [ 14 | "//pkg/apis/smith:go_default_library", 15 | "//vendor/github.com/atlassian/ctrl/apis/condition/v1:go_default_library", 16 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 17 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 18 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 19 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 20 | "//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", 21 | ], 22 | ) 23 | 24 | go_test( 25 | name = "go_default_test", 26 | size = "small", 27 | srcs = ["types_test.go"], 28 | embed = [":go_default_library"], 29 | race = "on", 30 | deps = [ 31 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 32 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/apis/smith/v1/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1 defines all of the versioned (v1) definitions 2 | // of the Smith model. 3 | // +groupName=smith.atlassian.com 4 | package v1 5 | -------------------------------------------------------------------------------- /pkg/apis/smith/v1/register.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/atlassian/smith/pkg/apis/smith" 5 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | // SchemeGroupVersion is group version used to register these objects. 11 | var SchemeGroupVersion = schema.GroupVersion{Group: smith.GroupName, Version: BundleResourceVersion} 12 | 13 | // Kind takes an unqualified kind and returns a Group qualified GroupKind. 14 | func Kind(kind string) schema.GroupKind { 15 | return SchemeGroupVersion.WithKind(kind).GroupKind() 16 | } 17 | 18 | var ( 19 | // SchemeBuilder needs to be exported as `SchemeBuilder` so 20 | // the code-generation can find it. 21 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 22 | // AddToScheme is exposed for API installation 23 | AddToScheme = SchemeBuilder.AddToScheme 24 | ) 25 | 26 | func addKnownTypes(scheme *runtime.Scheme) error { 27 | scheme.AddKnownTypes(SchemeGroupVersion, 28 | &Bundle{}, 29 | &BundleList{}, 30 | ) 31 | meta_v1.AddToGroupVersion(scheme, SchemeGroupVersion) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/apis/smith/v1/types_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | ) 7 | 8 | var _ runtime.Object = &BundleList{} 9 | var _ meta_v1.ListMetaAccessor = &BundleList{} 10 | 11 | var _ runtime.Object = &Bundle{} 12 | var _ meta_v1.ObjectMetaAccessor = &Bundle{} 13 | -------------------------------------------------------------------------------- /pkg/client/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["bundle.go"], 6 | importpath = "github.com/atlassian/smith/pkg/client", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//pkg/apis/smith/v1:go_default_library", 10 | "//pkg/client/clientset_generated/clientset:go_default_library", 11 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 12 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 13 | "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", 14 | "//vendor/k8s.io/client-go/tools/cache:go_default_library", 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/client/bundle.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "time" 5 | 6 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 7 | smithClientset "github.com/atlassian/smith/pkg/client/clientset_generated/clientset" 8 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/watch" 11 | "k8s.io/client-go/tools/cache" 12 | ) 13 | 14 | func BundleInformer(smithClient smithClientset.Interface, namespace string, resyncPeriod time.Duration) cache.SharedIndexInformer { 15 | bundlesAPI := smithClient.SmithV1().Bundles(namespace) 16 | return cache.NewSharedIndexInformer( 17 | &cache.ListWatch{ 18 | ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { 19 | return bundlesAPI.List(options) 20 | }, 21 | WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { 22 | return bundlesAPI.Watch(options) 23 | }, 24 | }, 25 | &smith_v1.Bundle{}, 26 | resyncPeriod, 27 | cache.Indexers{}) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "clientset.go", 7 | "doc.go", 8 | ], 9 | importpath = "github.com/atlassian/smith/pkg/client/clientset_generated/clientset", 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "//pkg/client/clientset_generated/clientset/typed/smith/v1:go_default_library", 13 | "//vendor/k8s.io/client-go/discovery:go_default_library", 14 | "//vendor/k8s.io/client-go/rest:go_default_library", 15 | "//vendor/k8s.io/client-go/util/flowcontrol:go_default_library", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/clientset.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | package clientset 6 | 7 | import ( 8 | "fmt" 9 | 10 | smithv1 "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/typed/smith/v1" 11 | discovery "k8s.io/client-go/discovery" 12 | rest "k8s.io/client-go/rest" 13 | flowcontrol "k8s.io/client-go/util/flowcontrol" 14 | ) 15 | 16 | type Interface interface { 17 | Discovery() discovery.DiscoveryInterface 18 | SmithV1() smithv1.SmithV1Interface 19 | } 20 | 21 | // Clientset contains the clients for groups. Each group has exactly one 22 | // version included in a Clientset. 23 | type Clientset struct { 24 | *discovery.DiscoveryClient 25 | smithV1 *smithv1.SmithV1Client 26 | } 27 | 28 | // SmithV1 retrieves the SmithV1Client 29 | func (c *Clientset) SmithV1() smithv1.SmithV1Interface { 30 | return c.smithV1 31 | } 32 | 33 | // Discovery retrieves the DiscoveryClient 34 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 35 | if c == nil { 36 | return nil 37 | } 38 | return c.DiscoveryClient 39 | } 40 | 41 | // NewForConfig creates a new Clientset for the given config. 42 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 43 | // NewForConfig will generate a rate-limiter in configShallowCopy. 44 | func NewForConfig(c *rest.Config) (*Clientset, error) { 45 | configShallowCopy := *c 46 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 47 | if configShallowCopy.Burst <= 0 { 48 | return nil, fmt.Errorf("Burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 49 | } 50 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 51 | } 52 | var cs Clientset 53 | var err error 54 | cs.smithV1, err = smithv1.NewForConfig(&configShallowCopy) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return &cs, nil 64 | } 65 | 66 | // NewForConfigOrDie creates a new Clientset for the given config and 67 | // panics if there is an error in the config. 68 | func NewForConfigOrDie(c *rest.Config) *Clientset { 69 | var cs Clientset 70 | cs.smithV1 = smithv1.NewForConfigOrDie(c) 71 | 72 | cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) 73 | return &cs 74 | } 75 | 76 | // New creates a new Clientset for the given RESTClient. 77 | func New(c rest.Interface) *Clientset { 78 | var cs Clientset 79 | cs.smithV1 = smithv1.New(c) 80 | 81 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 82 | return &cs 83 | } 84 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/doc.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | // This package has the automatically generated clientset. 6 | package clientset 7 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/fake/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "clientset_generated.go", 7 | "doc.go", 8 | "register.go", 9 | ], 10 | importpath = "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/fake", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//pkg/apis/smith/v1:go_default_library", 14 | "//pkg/client/clientset_generated/clientset:go_default_library", 15 | "//pkg/client/clientset_generated/clientset/typed/smith/v1:go_default_library", 16 | "//pkg/client/clientset_generated/clientset/typed/smith/v1/fake:go_default_library", 17 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 18 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 19 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 20 | "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", 21 | "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", 22 | "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", 23 | "//vendor/k8s.io/client-go/discovery:go_default_library", 24 | "//vendor/k8s.io/client-go/discovery/fake:go_default_library", 25 | "//vendor/k8s.io/client-go/testing:go_default_library", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | package fake 6 | 7 | import ( 8 | clientset "github.com/atlassian/smith/pkg/client/clientset_generated/clientset" 9 | smithv1 "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/typed/smith/v1" 10 | fakesmithv1 "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/typed/smith/v1/fake" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/watch" 13 | "k8s.io/client-go/discovery" 14 | fakediscovery "k8s.io/client-go/discovery/fake" 15 | "k8s.io/client-go/testing" 16 | ) 17 | 18 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 19 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 20 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 21 | // for a real clientset and is mostly useful in simple unit tests. 22 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 23 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 24 | for _, obj := range objects { 25 | if err := o.Add(obj); err != nil { 26 | panic(err) 27 | } 28 | } 29 | 30 | cs := &Clientset{tracker: o} 31 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 32 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 33 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 34 | gvr := action.GetResource() 35 | ns := action.GetNamespace() 36 | watch, err := o.Watch(gvr, ns) 37 | if err != nil { 38 | return false, nil, err 39 | } 40 | return true, watch, nil 41 | }) 42 | 43 | return cs 44 | } 45 | 46 | // Clientset implements clientset.Interface. Meant to be embedded into a 47 | // struct to get a default implementation. This makes faking out just the method 48 | // you want to test easier. 49 | type Clientset struct { 50 | testing.Fake 51 | discovery *fakediscovery.FakeDiscovery 52 | tracker testing.ObjectTracker 53 | } 54 | 55 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 56 | return c.discovery 57 | } 58 | 59 | func (c *Clientset) Tracker() testing.ObjectTracker { 60 | return c.tracker 61 | } 62 | 63 | var _ clientset.Interface = &Clientset{} 64 | 65 | // SmithV1 retrieves the SmithV1Client 66 | func (c *Clientset) SmithV1() smithv1.SmithV1Interface { 67 | return &fakesmithv1.FakeSmithV1{Fake: &c.Fake} 68 | } 69 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | // This package has the automatically generated fake clientset. 6 | package fake 7 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/fake/register.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | package fake 6 | 7 | import ( 8 | smithv1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | schema "k8s.io/apimachinery/pkg/runtime/schema" 12 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 13 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 14 | ) 15 | 16 | var scheme = runtime.NewScheme() 17 | var codecs = serializer.NewCodecFactory(scheme) 18 | var parameterCodec = runtime.NewParameterCodec(scheme) 19 | var localSchemeBuilder = runtime.SchemeBuilder{ 20 | smithv1.AddToScheme, 21 | } 22 | 23 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 24 | // of clientsets, like in: 25 | // 26 | // import ( 27 | // "k8s.io/client-go/kubernetes" 28 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 29 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 30 | // ) 31 | // 32 | // kclientset, _ := kubernetes.NewForConfig(c) 33 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 34 | // 35 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 36 | // correctly. 37 | var AddToScheme = localSchemeBuilder.AddToScheme 38 | 39 | func init() { 40 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 41 | utilruntime.Must(AddToScheme(scheme)) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/scheme/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "doc.go", 7 | "register.go", 8 | ], 9 | importpath = "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/scheme", 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "//pkg/apis/smith/v1:go_default_library", 13 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 14 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 15 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 16 | "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", 17 | "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/scheme/doc.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | // This package contains the scheme of the automatically generated clientset. 6 | package scheme 7 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/scheme/register.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | package scheme 6 | 7 | import ( 8 | smithv1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | schema "k8s.io/apimachinery/pkg/runtime/schema" 12 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 13 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 14 | ) 15 | 16 | var Scheme = runtime.NewScheme() 17 | var Codecs = serializer.NewCodecFactory(Scheme) 18 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 19 | var localSchemeBuilder = runtime.SchemeBuilder{ 20 | smithv1.AddToScheme, 21 | } 22 | 23 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 24 | // of clientsets, like in: 25 | // 26 | // import ( 27 | // "k8s.io/client-go/kubernetes" 28 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 29 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 30 | // ) 31 | // 32 | // kclientset, _ := kubernetes.NewForConfig(c) 33 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 34 | // 35 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 36 | // correctly. 37 | var AddToScheme = localSchemeBuilder.AddToScheme 38 | 39 | func init() { 40 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 41 | utilruntime.Must(AddToScheme(Scheme)) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/typed/smith/v1/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "bundle.go", 7 | "doc.go", 8 | "generated_expansion.go", 9 | "smith_client.go", 10 | ], 11 | importpath = "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/typed/smith/v1", 12 | visibility = ["//visibility:public"], 13 | deps = [ 14 | "//pkg/apis/smith/v1:go_default_library", 15 | "//pkg/client/clientset_generated/clientset/scheme:go_default_library", 16 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 17 | "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", 18 | "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", 19 | "//vendor/k8s.io/client-go/rest:go_default_library", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/typed/smith/v1/doc.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | // This package has the automatically generated typed clients. 6 | package v1 7 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/typed/smith/v1/fake/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "doc.go", 7 | "fake_bundle.go", 8 | "fake_smith_client.go", 9 | ], 10 | importpath = "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/typed/smith/v1/fake", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//pkg/apis/smith/v1:go_default_library", 14 | "//pkg/client/clientset_generated/clientset/typed/smith/v1:go_default_library", 15 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 16 | "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", 17 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 18 | "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", 19 | "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", 20 | "//vendor/k8s.io/client-go/rest:go_default_library", 21 | "//vendor/k8s.io/client-go/testing:go_default_library", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/typed/smith/v1/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | // Package fake has the automatically generated clients. 6 | package fake 7 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/typed/smith/v1/fake/fake_smith_client.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | package fake 6 | 7 | import ( 8 | v1 "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/typed/smith/v1" 9 | rest "k8s.io/client-go/rest" 10 | testing "k8s.io/client-go/testing" 11 | ) 12 | 13 | type FakeSmithV1 struct { 14 | *testing.Fake 15 | } 16 | 17 | func (c *FakeSmithV1) Bundles(namespace string) v1.BundleInterface { 18 | return &FakeBundles{c, namespace} 19 | } 20 | 21 | // RESTClient returns a RESTClient that is used to communicate 22 | // with API server by this client implementation. 23 | func (c *FakeSmithV1) RESTClient() rest.Interface { 24 | var ret *rest.RESTClient 25 | return ret 26 | } 27 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/typed/smith/v1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | package v1 6 | 7 | type BundleExpansion interface{} 8 | -------------------------------------------------------------------------------- /pkg/client/clientset_generated/clientset/typed/smith/v1/smith_client.go: -------------------------------------------------------------------------------- 1 | // Generated file, do not modify manually! 2 | 3 | // Code generated by client-gen. DO NOT EDIT. 4 | 5 | package v1 6 | 7 | import ( 8 | v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/client/clientset_generated/clientset/scheme" 10 | rest "k8s.io/client-go/rest" 11 | ) 12 | 13 | type SmithV1Interface interface { 14 | RESTClient() rest.Interface 15 | BundlesGetter 16 | } 17 | 18 | // SmithV1Client is used to interact with features provided by the smith.atlassian.com group. 19 | type SmithV1Client struct { 20 | restClient rest.Interface 21 | } 22 | 23 | func (c *SmithV1Client) Bundles(namespace string) BundleInterface { 24 | return newBundles(c, namespace) 25 | } 26 | 27 | // NewForConfig creates a new SmithV1Client for the given config. 28 | func NewForConfig(c *rest.Config) (*SmithV1Client, error) { 29 | config := *c 30 | if err := setConfigDefaults(&config); err != nil { 31 | return nil, err 32 | } 33 | client, err := rest.RESTClientFor(&config) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return &SmithV1Client{client}, nil 38 | } 39 | 40 | // NewForConfigOrDie creates a new SmithV1Client for the given config and 41 | // panics if there is an error in the config. 42 | func NewForConfigOrDie(c *rest.Config) *SmithV1Client { 43 | client, err := NewForConfig(c) 44 | if err != nil { 45 | panic(err) 46 | } 47 | return client 48 | } 49 | 50 | // New creates a new SmithV1Client for the given RESTClient. 51 | func New(c rest.Interface) *SmithV1Client { 52 | return &SmithV1Client{c} 53 | } 54 | 55 | func setConfigDefaults(config *rest.Config) error { 56 | gv := v1.SchemeGroupVersion 57 | config.GroupVersion = &gv 58 | config.APIPath = "/apis" 59 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 60 | 61 | if config.UserAgent == "" { 62 | config.UserAgent = rest.DefaultKubernetesUserAgent() 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // RESTClient returns a RESTClient that is used to communicate 69 | // with API server by this client implementation. 70 | func (c *SmithV1Client) RESTClient() rest.Interface { 71 | if c == nil { 72 | return nil 73 | } 74 | return c.restClient 75 | } 76 | -------------------------------------------------------------------------------- /pkg/client/smart/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "discovery.go", 7 | "smart.go", 8 | ], 9 | importpath = "github.com/atlassian/smith/pkg/client/smart", 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "//vendor/github.com/pkg/errors:go_default_library", 13 | "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", 14 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 15 | "//vendor/k8s.io/client-go/discovery:go_default_library", 16 | "//vendor/k8s.io/client-go/dynamic:go_default_library", 17 | ], 18 | ) 19 | 20 | go_test( 21 | name = "go_default_test", 22 | size = "small", 23 | srcs = ["discovery_test.go"], 24 | embed = [":go_default_library"], 25 | race = "on", 26 | deps = ["//vendor/k8s.io/client-go/discovery:go_default_library"], 27 | ) 28 | -------------------------------------------------------------------------------- /pkg/client/smart/discovery.go: -------------------------------------------------------------------------------- 1 | package smart 2 | 3 | import "k8s.io/client-go/discovery" 4 | 5 | // TODO implement caching and invalidation 6 | 7 | type CachedDiscoveryClient struct { 8 | discovery.DiscoveryInterface 9 | } 10 | 11 | // Fresh returns true if no cached data was used that had been retrieved before the instantiation. 12 | func (c *CachedDiscoveryClient) Fresh() bool { 13 | return true 14 | } 15 | 16 | // Invalidate enforces that no cached data is used in the future that is older than the current time. 17 | func (c *CachedDiscoveryClient) Invalidate() { 18 | } 19 | -------------------------------------------------------------------------------- /pkg/client/smart/discovery_test.go: -------------------------------------------------------------------------------- 1 | package smart 2 | 3 | import "k8s.io/client-go/discovery" 4 | 5 | var ( 6 | _ discovery.CachedDiscoveryInterface = &CachedDiscoveryClient{} 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/client/smart/smart.go: -------------------------------------------------------------------------------- 1 | package smart 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "k8s.io/apimachinery/pkg/api/meta" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "k8s.io/client-go/dynamic" 8 | ) 9 | 10 | type DynamicClient struct { 11 | DynamicClient dynamic.Interface 12 | RESTMapper meta.RESTMapper 13 | } 14 | 15 | func (c *DynamicClient) ForGVK(gvk schema.GroupVersionKind, namespace string) (dynamic.ResourceInterface, error) { 16 | rm, err := c.RESTMapper.RESTMapping(gvk.GroupKind(), gvk.Version) 17 | if err != nil { 18 | return nil, errors.Wrapf(err, "failed to get rest mapping for %s", gvk) 19 | } 20 | return c.DynamicClient.Resource(rm.Resource).Namespace(namespace), nil 21 | } 22 | -------------------------------------------------------------------------------- /pkg/controller/bundlec/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "bundle_sync_task.go", 7 | "controller.go", 8 | "controller_crd_event_handler.go", 9 | "controller_worker.go", 10 | "finalizers.go", 11 | "resource_sync_task.go", 12 | "spec_processor.go", 13 | "types.go", 14 | ], 15 | importpath = "github.com/atlassian/smith/pkg/controller/bundlec", 16 | visibility = ["//visibility:public"], 17 | deps = [ 18 | "//:go_default_library", 19 | "//pkg/apis/smith/v1:go_default_library", 20 | "//pkg/client/clientset_generated/clientset/typed/smith/v1:go_default_library", 21 | "//pkg/plugin:go_default_library", 22 | "//pkg/resources:go_default_library", 23 | "//pkg/statuschecker:go_default_library", 24 | "//pkg/store:go_default_library", 25 | "//pkg/util:go_default_library", 26 | "//pkg/util/graph:go_default_library", 27 | "//pkg/util/logz:go_default_library", 28 | "//vendor/github.com/ash2k/stager/wait:go_default_library", 29 | "//vendor/github.com/atlassian/ctrl:go_default_library", 30 | "//vendor/github.com/atlassian/ctrl/apis/condition/v1:go_default_library", 31 | "//vendor/github.com/atlassian/ctrl/handlers:go_default_library", 32 | "//vendor/github.com/atlassian/ctrl/logz:go_default_library", 33 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1:go_default_library", 34 | "//vendor/github.com/pkg/errors:go_default_library", 35 | "//vendor/github.com/prometheus/client_golang/prometheus:go_default_library", 36 | "//vendor/go.uber.org/zap:go_default_library", 37 | "//vendor/k8s.io/api/apps/v1:go_default_library", 38 | "//vendor/k8s.io/api/core/v1:go_default_library", 39 | "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", 40 | "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", 41 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 42 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 43 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 44 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 45 | "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", 46 | "//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library", 47 | "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", 48 | "//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", 49 | "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", 50 | "//vendor/k8s.io/client-go/dynamic:go_default_library", 51 | "//vendor/k8s.io/client-go/kubernetes:go_default_library", 52 | "//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", 53 | "//vendor/k8s.io/client-go/tools/cache:go_default_library", 54 | "//vendor/k8s.io/client-go/tools/record:go_default_library", 55 | ], 56 | ) 57 | 58 | go_test( 59 | name = "go_default_test", 60 | size = "small", 61 | srcs = [ 62 | "controller_worker_test.go", 63 | "spec_processor_test.go", 64 | ], 65 | embed = [":go_default_library"], 66 | race = "on", 67 | deps = [ 68 | "//pkg/apis/smith/v1:go_default_library", 69 | "//pkg/util/graph:go_default_library", 70 | "//vendor/github.com/stretchr/testify/assert:go_default_library", 71 | "//vendor/github.com/stretchr/testify/require:go_default_library", 72 | "//vendor/k8s.io/api/core/v1:go_default_library", 73 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 74 | ], 75 | ) 76 | -------------------------------------------------------------------------------- /pkg/controller/bundlec/controller_worker.go: -------------------------------------------------------------------------------- 1 | package bundlec 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/atlassian/ctrl" 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/pkg/errors" 9 | "go.uber.org/zap" 10 | api_errors "k8s.io/apimachinery/pkg/api/errors" 11 | ) 12 | 13 | func (c *Controller) Process(pctx *ctrl.ProcessContext) (bool /*external*/, bool /*retriable*/, error) { 14 | return c.ProcessBundle(pctx.Logger, pctx.Object.(*smith_v1.Bundle)) 15 | } 16 | 17 | // ProcessBundle is only visible for testing purposes. Should not be called directly. 18 | func (c *Controller) ProcessBundle(logger *zap.Logger, bundle *smith_v1.Bundle) (bool /*external*/, bool /*retriable*/, error) { 19 | st := bundleSyncTask{ 20 | logger: logger, 21 | bundleClient: c.BundleClient, 22 | smartClient: c.SmartClient, 23 | checker: c.Rc, 24 | store: c.Store, 25 | specChecker: c.SpecChecker, 26 | bundle: bundle, 27 | pluginContainers: c.PluginContainers, 28 | scheme: c.Scheme, 29 | catalog: c.Catalog, 30 | bundleTransitionCounter: c.BundleTransitionCounter, 31 | bundleResourceTransitionCounter: c.BundleResourceTransitionCounter, 32 | recorder: c.Recorder, 33 | } 34 | 35 | var external bool 36 | var retriable bool 37 | var err error 38 | if st.bundle.DeletionTimestamp != nil { 39 | external, retriable, err = st.processDeleted() 40 | } else { 41 | external, retriable, err = st.processNormal() 42 | } 43 | if err != nil { 44 | cause := errors.Cause(err) 45 | // short circuit on conflicts 46 | if api_errors.IsConflict(cause) { 47 | return external, retriable, err 48 | } 49 | // proceed to handleProcessResult() for all other errors 50 | } 51 | 52 | // Updates bundle status 53 | handleProcessRetriable, handleProcessErr := st.handleProcessResult(retriable, err) 54 | 55 | // Inspect the resources for failures. They can fail for many different reasons. 56 | // The priority of errors to bubble up to the ctrl layer are: 57 | // 1. processDeleted/processNormal errors 58 | // 2. Internal resource processing errors are raised first 59 | // 3. External resource processing errors are raised last 60 | // 4. handleProcessResult errors of any sort. 61 | 62 | // Handle the errors from processDeleted/processNormal, taking precedence 63 | // over the handleProcessErr if any. 64 | if err != nil { 65 | if handleProcessErr != nil { 66 | st.logger.Error("Error processing Bundle", zap.Error(handleProcessErr)) 67 | } 68 | 69 | return external, retriable || handleProcessRetriable, err 70 | } 71 | 72 | // Inspect resources, returning an error if necessary 73 | allExternalErrors := true 74 | hasRetriableResourceErr := false 75 | var failedResources []string 76 | for resName, resInfo := range st.processedResources { 77 | resErr := resInfo.fetchError() 78 | 79 | if resErr != nil { 80 | allExternalErrors = allExternalErrors && resErr.isExternalError 81 | hasRetriableResourceErr = hasRetriableResourceErr || resErr.isRetriableError 82 | failedResources = append(failedResources, string(resName)) 83 | } 84 | } 85 | if len(failedResources) > 0 { 86 | if handleProcessErr != nil { 87 | st.logger.Error("Error processing Bundle", zap.Error(handleProcessErr)) 88 | } 89 | // stable output 90 | sort.Strings(failedResources) 91 | err := errors.Errorf("error processing resource(s): %q", failedResources) 92 | return allExternalErrors, hasRetriableResourceErr || handleProcessRetriable, err 93 | } 94 | 95 | // Otherwise, return the result from handleProcessResult 96 | return false, handleProcessRetriable, handleProcessErr 97 | } 98 | -------------------------------------------------------------------------------- /pkg/controller/bundlec/controller_worker_test.go: -------------------------------------------------------------------------------- 1 | package bundlec 2 | 3 | import ( 4 | "testing" 5 | 6 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 7 | "github.com/atlassian/smith/pkg/util/graph" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBundleSort(t *testing.T) { 13 | t.Parallel() 14 | bundle := smith_v1.Bundle{ 15 | Spec: smith_v1.BundleSpec{ 16 | Resources: []smith_v1.Resource{ 17 | { 18 | Name: "a", 19 | References: []smith_v1.Reference{ 20 | { 21 | Resource: "c", 22 | }, 23 | }, 24 | }, 25 | { 26 | Name: "b", 27 | }, 28 | { 29 | Name: "c", 30 | References: []smith_v1.Reference{ 31 | { 32 | Resource: "b", 33 | }, 34 | }, 35 | }, 36 | { 37 | Name: "d", 38 | References: []smith_v1.Reference{ 39 | { 40 | Resource: "e", 41 | }, 42 | }, 43 | }, 44 | { 45 | Name: "e", 46 | }, 47 | }, 48 | }, 49 | } 50 | _, sorted, err := sortBundle(&bundle) 51 | require.NoError(t, err) 52 | 53 | assert.EqualValues(t, []graph.V{smith_v1.ResourceName("b"), smith_v1.ResourceName("c"), smith_v1.ResourceName("a"), smith_v1.ResourceName("e"), smith_v1.ResourceName("d")}, sorted) 54 | } 55 | 56 | func TestBundleSortMissingDependency(t *testing.T) { 57 | t.Parallel() 58 | bundle := smith_v1.Bundle{ 59 | Spec: smith_v1.BundleSpec{ 60 | Resources: []smith_v1.Resource{ 61 | { 62 | Name: "a", 63 | References: []smith_v1.Reference{ 64 | { 65 | Resource: "x", 66 | }, 67 | }, 68 | }, 69 | { 70 | Name: "b", 71 | }, 72 | { 73 | Name: "c", 74 | References: []smith_v1.Reference{ 75 | { 76 | Resource: "b", 77 | }, 78 | }, 79 | }, 80 | { 81 | Name: "d", 82 | References: []smith_v1.Reference{ 83 | { 84 | Resource: "e", 85 | }, 86 | }, 87 | }, 88 | { 89 | Name: "e", 90 | }, 91 | }, 92 | }, 93 | } 94 | _, sorted, err := sortBundle(&bundle) 95 | require.EqualError(t, err, "vertex \"x\" not found", "%v", sorted) 96 | } 97 | 98 | func TestBundleSortSelfReference(t *testing.T) { 99 | t.Parallel() 100 | bundle := smith_v1.Bundle{ 101 | Spec: smith_v1.BundleSpec{ 102 | Resources: []smith_v1.Resource{ 103 | { 104 | Name: "a", 105 | References: []smith_v1.Reference{ 106 | { 107 | Resource: "a", 108 | }, 109 | }, 110 | }, 111 | { 112 | Name: "b", 113 | }, 114 | { 115 | Name: "c", 116 | References: []smith_v1.Reference{ 117 | { 118 | Resource: "b", 119 | }, 120 | }, 121 | }, 122 | { 123 | Name: "d", 124 | References: []smith_v1.Reference{ 125 | { 126 | Resource: "e", 127 | }, 128 | }, 129 | }, 130 | { 131 | Name: "e", 132 | }, 133 | }, 134 | }, 135 | } 136 | _, sorted, err := sortBundle(&bundle) 137 | require.EqualError(t, err, "cycle error: [a a]", "%v", sorted) 138 | } 139 | -------------------------------------------------------------------------------- /pkg/controller/bundlec/finalizers.go: -------------------------------------------------------------------------------- 1 | package bundlec 2 | 3 | import ( 4 | "github.com/atlassian/smith" 5 | "github.com/atlassian/smith/pkg/resources" 6 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | const ( 10 | FinalizerDeleteResources = smith.Domain + "/deleteResources" 11 | ) 12 | 13 | func hasDeleteResourcesFinalizer(accessor meta_v1.Object) bool { 14 | return resources.HasFinalizer(accessor, FinalizerDeleteResources) 15 | } 16 | 17 | func addDeleteResourcesFinalizer(finalizers []string) []string { 18 | return append(finalizers, FinalizerDeleteResources) 19 | } 20 | 21 | func removeDeleteResourcesFinalizer(finalizers []string) []string { 22 | newFinalizers := make([]string, 0, len(finalizers)) 23 | for _, finalizer := range finalizers { 24 | if finalizer == FinalizerDeleteResources { 25 | continue 26 | } 27 | newFinalizers = append(newFinalizers, finalizer) 28 | } 29 | return newFinalizers 30 | } 31 | -------------------------------------------------------------------------------- /pkg/controller/bundlec/types.go: -------------------------------------------------------------------------------- 1 | package bundlec 2 | 3 | import ( 4 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 5 | "go.uber.org/zap" 6 | apiext_v1b1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/apimachinery/pkg/types" 11 | "k8s.io/client-go/dynamic" 12 | "k8s.io/client-go/tools/cache" 13 | ) 14 | 15 | type SpecChecker interface { 16 | // BeforeCreate pre-processes object specification and returns an updated version. 17 | BeforeCreate(logger *zap.Logger, spec *unstructured.Unstructured) (*unstructured.Unstructured /*updatedSpec*/, error) 18 | CompareActualVsSpec(logger *zap.Logger, spec, actual runtime.Object) (updatedSpec *unstructured.Unstructured, match bool, diff string, err error) 19 | } 20 | 21 | type Store interface { 22 | Get(gvk schema.GroupVersionKind, namespace, name string) (obj runtime.Object, exists bool, err error) 23 | ObjectsControlledBy(namespace string, uid types.UID) ([]runtime.Object, error) 24 | AddInformer(schema.GroupVersionKind, cache.SharedIndexInformer) error 25 | RemoveInformer(schema.GroupVersionKind) bool 26 | } 27 | 28 | type BundleStore interface { 29 | // Get returns Bundle based on its namespace and name. 30 | Get(namespace, bundleName string) (*smith_v1.Bundle, error) 31 | // GetBundlesByCrd returns Bundles which have a resource defined by CRD. 32 | GetBundlesByCrd(*apiext_v1b1.CustomResourceDefinition) ([]*smith_v1.Bundle, error) 33 | // GetBundlesByObject returns Bundles which have a resource of a particular group/kind with a name in a namespace. 34 | GetBundlesByObject(gk schema.GroupKind, namespace, name string) ([]*smith_v1.Bundle, error) 35 | } 36 | 37 | type SmartClient interface { 38 | ForGVK(gvk schema.GroupVersionKind, namespace string) (dynamic.ResourceInterface, error) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/actual_object_passed_to_plugin_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "testing" 5 | 6 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 7 | "github.com/atlassian/smith/pkg/controller/bundlec" 8 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/util/sets" 11 | ) 12 | 13 | // Should pass actual object to the plugin 14 | func TestActualObjectPassedToPlugin(t *testing.T) { 15 | t.Parallel() 16 | tc := testCase{ 17 | mainClientObjects: []runtime.Object{ 18 | configMapNeedsUpdate(), 19 | }, 20 | bundle: &smith_v1.Bundle{ 21 | ObjectMeta: meta_v1.ObjectMeta{ 22 | Name: bundle1, 23 | Namespace: testNamespace, 24 | UID: bundle1uid, 25 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 26 | }, 27 | Spec: smith_v1.BundleSpec{ 28 | Resources: []smith_v1.Resource{ 29 | { 30 | Name: resP1, 31 | Spec: smith_v1.ResourceSpec{ 32 | Plugin: &smith_v1.PluginSpec{ 33 | Name: pluginSimpleConfigMap, 34 | ObjectName: mapNeedsAnUpdate, 35 | Spec: map[string]interface{}{ 36 | "actualShouldExist": true, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | appName: testAppName, 45 | namespace: testNamespace, 46 | expectedActions: sets.NewString("PUT=/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate), 47 | testHandler: fakeActionHandler{ 48 | response: map[path]fakeResponse{ 49 | { 50 | method: "PUT", 51 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate, 52 | }: configMapNeedsUpdateResponse(bundle1, bundle1uid), 53 | }, 54 | }, 55 | plugins: map[smith_v1.PluginName]func(*testing.T) testingPlugin{ 56 | pluginSimpleConfigMap: simpleConfigMapPlugin, 57 | }, 58 | pluginsShouldBeInvoked: sets.NewString(string(pluginSimpleConfigMap)), 59 | } 60 | tc.run(t) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/cr_in_another_namespace_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | sleeper_v1 "github.com/atlassian/smith/examples/sleeper/pkg/apis/sleeper/v1" 10 | "github.com/atlassian/smith/pkg/controller/bundlec" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/util/sets" 13 | ) 14 | 15 | // Should be able to list CRs in another namespace 16 | func TestCrInAnotherNamespace(t *testing.T) { 17 | t.Parallel() 18 | tc := testCase{ 19 | apiExtClientObjects: []runtime.Object{ 20 | sleeperCrdWithStatus(), 21 | }, 22 | expectedActions: sets.NewString( 23 | "GET=/apis/"+sleeper_v1.SleeperResourceGroupVersion+"/namespaces/"+testNamespace+"/"+sleeper_v1.SleeperResourcePlural+ 24 | "=limit=500&resourceVersion=0", 25 | "GET=/apis/"+sleeper_v1.SleeperResourceGroupVersion+"/namespaces/"+testNamespace+"/"+sleeper_v1.SleeperResourcePlural+ 26 | "=watch", 27 | ), 28 | appName: testAppName, 29 | namespace: testNamespace, 30 | test: func(t *testing.T, ctx context.Context, bundleController *bundlec.Controller, testcase *testCase) { 31 | bundleController.Run(ctx) 32 | }, 33 | testHandler: fakeActionHandler{ 34 | response: map[path]fakeResponse{ 35 | { 36 | method: "GET", 37 | path: "/apis/" + sleeper_v1.SleeperResourceGroupVersion + "/namespaces/" + testNamespace + "/" + sleeper_v1.SleeperResourcePlural, 38 | }: { 39 | statusCode: http.StatusOK, 40 | content: []byte(`{"kind": "List", "items": []}`), 41 | }, 42 | { 43 | method: "GET", 44 | watch: true, 45 | path: "/apis/" + sleeper_v1.SleeperResourceGroupVersion + "/namespaces/" + testNamespace + "/" + sleeper_v1.SleeperResourcePlural, 46 | }: { 47 | statusCode: http.StatusOK, 48 | }, 49 | }, 50 | }, 51 | testTimeout: 500 * time.Millisecond, 52 | } 53 | tc.run(t) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/delay_postpone_delete_removed_object_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/util/sets" 13 | ) 14 | 15 | // Should postpone the deletion of the controlled object 16 | // if deletion delay hasn't expired yet 17 | func TestDelayDeletePostpone(t *testing.T) { 18 | t.Parallel() 19 | m1 := configMapNeedsUpdate() 20 | if m1.Annotations == nil { 21 | m1.Annotations = make(map[string]string) 22 | } 23 | m1.Annotations["smith.atlassian.com/deletionDelay"] = "1h" 24 | m1.Annotations["smith.atlassian.com/deletionTimestamp"] = time.Now().UTC().Format(time.RFC3339) 25 | tc := testCase{ 26 | mainClientObjects: []runtime.Object{ 27 | m1, 28 | }, 29 | bundle: &smith_v1.Bundle{ 30 | ObjectMeta: meta_v1.ObjectMeta{ 31 | Name: bundle1, 32 | Namespace: testNamespace, 33 | UID: bundle1uid, 34 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 35 | }, 36 | }, 37 | expectedActions: sets.NewString(), // No actions 38 | appName: testAppName, 39 | namespace: meta_v1.NamespaceAll, 40 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 41 | tc.defaultTest(t, ctx, cntrlr) 42 | tc.assertObjectsToBeDeleted(t, m1) 43 | }, 44 | } 45 | tc.run(t) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/delay_proceed_delete_removed_object_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 10 | "github.com/atlassian/smith/pkg/controller/bundlec" 11 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/util/sets" 14 | ) 15 | 16 | // Should proceed with deletion of the controlled object 17 | // after the deletion delay has expired 18 | func TestDelayDeleteProceed(t *testing.T) { 19 | t.Parallel() 20 | m1 := configMapNeedsUpdate() 21 | if m1.Annotations == nil { 22 | m1.Annotations = make(map[string]string) 23 | } 24 | m1.Annotations["smith.atlassian.com/deletionDelay"] = "1h" 25 | m1.Annotations["smith.atlassian.com/deletionTimestamp"] = time.Now().Add(-2 * time.Hour).UTC().Format(time.RFC3339) 26 | tc := testCase{ 27 | mainClientObjects: []runtime.Object{ 28 | m1, 29 | }, 30 | bundle: &smith_v1.Bundle{ 31 | ObjectMeta: meta_v1.ObjectMeta{ 32 | Name: bundle1, 33 | Namespace: testNamespace, 34 | UID: bundle1uid, 35 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 36 | }, 37 | }, 38 | expectedActions: sets.NewString("DELETE=/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate), 39 | appName: testAppName, 40 | namespace: meta_v1.NamespaceAll, 41 | testHandler: fakeActionHandler{ 42 | response: map[path]fakeResponse{ 43 | { 44 | method: "DELETE", 45 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate, 46 | }: { 47 | statusCode: http.StatusOK, 48 | }, 49 | }, 50 | }, 51 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 52 | tc.defaultTest(t, ctx, cntrlr) 53 | tc.assertObjectsToBeDeleted(t, m1) 54 | }, 55 | } 56 | tc.run(t) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/delay_start_delete_removed_object_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/atlassian/smith/pkg/controller/bundlec" 9 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/util/sets" 12 | ) 13 | 14 | // Should initiate a delay for deletion controlled object that is not in the bundle 15 | func TestDelayDeleteStart(t *testing.T) { 16 | t.Parallel() 17 | m1 := configMapNeedsUpdate() 18 | if m1.Annotations == nil { 19 | m1.Annotations = make(map[string]string) 20 | } 21 | m1.Annotations["smith.atlassian.com/deletionDelay"] = "1h" 22 | tc := testCase{ 23 | mainClientObjects: []runtime.Object{ 24 | m1, 25 | }, 26 | bundle: &smith_v1.Bundle{ 27 | ObjectMeta: meta_v1.ObjectMeta{ 28 | Name: bundle1, 29 | Namespace: testNamespace, 30 | UID: bundle1uid, 31 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 32 | }, 33 | }, 34 | expectedActions: sets.NewString("PUT=/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate), 35 | appName: testAppName, 36 | namespace: meta_v1.NamespaceAll, 37 | testHandler: fakeActionHandler{ 38 | response: map[path]fakeResponse{ 39 | { 40 | method: "PUT", 41 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate, 42 | }: configMapNeedsUpdateResponse(bundle1, bundle1uid), 43 | }, 44 | }, 45 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 46 | tc.defaultTest(t, ctx, cntrlr) 47 | tc.assertObjectsToBeDeleted(t, m1) 48 | }, 49 | } 50 | tc.run(t) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/delete_removed_object_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/util/sets" 13 | ) 14 | 15 | // Should delete controlled object that is not in the bundle 16 | func TestDeleteRemovedObject(t *testing.T) { 17 | t.Parallel() 18 | m1 := configMapNeedsUpdate() 19 | m2 := configMapMarkedForDeletion() 20 | tc := testCase{ 21 | mainClientObjects: []runtime.Object{ 22 | m1, 23 | m2, 24 | }, 25 | bundle: &smith_v1.Bundle{ 26 | ObjectMeta: meta_v1.ObjectMeta{ 27 | Name: bundle1, 28 | Namespace: testNamespace, 29 | UID: bundle1uid, 30 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 31 | }, 32 | }, 33 | expectedActions: sets.NewString("DELETE=/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate), 34 | appName: testAppName, 35 | namespace: meta_v1.NamespaceAll, 36 | testHandler: fakeActionHandler{ 37 | response: map[path]fakeResponse{ 38 | { 39 | method: "DELETE", 40 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate, 41 | }: { 42 | statusCode: http.StatusOK, 43 | }, 44 | }, 45 | }, 46 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 47 | tc.defaultTest(t, ctx, cntrlr) 48 | tc.assertObjectsToBeDeleted(t, m1, m2) 49 | }, 50 | } 51 | tc.run(t) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/deleted_bundle_foreground_deletion_noop_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/atlassian/smith/pkg/controller/bundlec" 9 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | core_v1 "k8s.io/api/core/v1" 13 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | kube_testing "k8s.io/client-go/testing" 16 | ) 17 | 18 | // Should not perform any creates/updates/deletes on resources after bundle 19 | // is marked with deletionTimestamp and "foregroundDeletion" finalizer 20 | func TestNoActionsForResourcesWhenForegroundDeletion(t *testing.T) { 21 | t.Parallel() 22 | now := meta_v1.Now() 23 | tc := testCase{ 24 | mainClientObjects: []runtime.Object{ 25 | configMapNeedsDelete(), 26 | configMapNeedsUpdate(), 27 | }, 28 | scClientObjects: []runtime.Object{ 29 | serviceInstance(false, false, true), 30 | }, 31 | bundle: &smith_v1.Bundle{ 32 | ObjectMeta: meta_v1.ObjectMeta{ 33 | Name: bundle1, 34 | Namespace: testNamespace, 35 | UID: bundle1uid, 36 | DeletionTimestamp: &now, 37 | Finalizers: []string{meta_v1.FinalizerDeleteDependents}, 38 | }, 39 | Spec: smith_v1.BundleSpec{ 40 | Resources: []smith_v1.Resource{ 41 | { 42 | Name: resSi1, 43 | Spec: smith_v1.ResourceSpec{ 44 | Object: &sc_v1b1.ServiceInstance{ 45 | TypeMeta: meta_v1.TypeMeta{ 46 | Kind: "ServiceInstance", 47 | APIVersion: sc_v1b1.SchemeGroupVersion.String(), 48 | }, 49 | ObjectMeta: meta_v1.ObjectMeta{ 50 | Name: si1, 51 | }, 52 | Spec: serviceInstanceSpec, 53 | }, 54 | }, 55 | }, 56 | { 57 | Name: resMapNeedsAnUpdate, 58 | References: []smith_v1.Reference{ 59 | {Resource: resSi1}, 60 | }, 61 | Spec: smith_v1.ResourceSpec{ 62 | Object: &core_v1.ConfigMap{ 63 | TypeMeta: meta_v1.TypeMeta{ 64 | Kind: "ConfigMap", 65 | APIVersion: core_v1.SchemeGroupVersion.String(), 66 | }, 67 | ObjectMeta: meta_v1.ObjectMeta{ 68 | Name: mapNeedsAnUpdate, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | appName: testAppName, 77 | namespace: testNamespace, 78 | enableServiceCatalog: false, 79 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 80 | tc.defaultTest(t, ctx, cntrlr) 81 | 82 | actions := tc.smithFake.Actions() 83 | require.Len(t, actions, 2) 84 | assert.Implements(t, (*kube_testing.ListAction)(nil), actions[0]) 85 | assert.Implements(t, (*kube_testing.WatchAction)(nil), actions[1]) 86 | 87 | assert.Empty(t, tc.bundle.Status.ObjectsToDelete) 88 | }, 89 | } 90 | tc.run(t) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/deleted_bundle_manual_delete_resources_fail_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | core_v1 "k8s.io/api/core/v1" 13 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/apimachinery/pkg/util/sets" 16 | kube_testing "k8s.io/client-go/testing" 17 | ) 18 | 19 | // Should keep the "deleteResources" finalizer if some resources can't be deleted 20 | func TestKeepFinalizerWhenResourceDeletionFails(t *testing.T) { 21 | t.Parallel() 22 | now := meta_v1.Now() 23 | tc := testCase{ 24 | mainClientObjects: []runtime.Object{ 25 | configMapNeedsDelete(), 26 | configMapNeedsUpdate(), 27 | }, 28 | scClientObjects: []runtime.Object{ 29 | serviceInstance(false, false, true), 30 | }, 31 | bundle: &smith_v1.Bundle{ 32 | ObjectMeta: meta_v1.ObjectMeta{ 33 | Name: bundle1, 34 | Namespace: testNamespace, 35 | UID: bundle1uid, 36 | DeletionTimestamp: &now, 37 | // Finalizer to enforce manual deletion 38 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 39 | }, 40 | Spec: smith_v1.BundleSpec{ 41 | Resources: []smith_v1.Resource{ 42 | { 43 | Name: resMapNeedsAnUpdate, 44 | Spec: smith_v1.ResourceSpec{ 45 | Object: &core_v1.ConfigMap{ 46 | TypeMeta: meta_v1.TypeMeta{ 47 | Kind: "ConfigMap", 48 | APIVersion: core_v1.SchemeGroupVersion.String(), 49 | }, 50 | ObjectMeta: meta_v1.ObjectMeta{ 51 | Name: mapNeedsAnUpdate, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | expectedActions: sets.NewString( 60 | "DELETE=/api/v1/namespaces/"+testNamespace+"/configmaps/"+mapNeedsAnUpdate, 61 | "DELETE=/api/v1/namespaces/"+testNamespace+"/configmaps/"+mapNeedsDelete, 62 | ), 63 | testHandler: fakeActionHandler{ 64 | response: map[path]fakeResponse{ 65 | { 66 | method: "DELETE", 67 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate, 68 | }: { 69 | statusCode: http.StatusOK, 70 | }, 71 | { 72 | method: "DELETE", 73 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsDelete, 74 | }: { 75 | statusCode: http.StatusInternalServerError, 76 | content: []byte("something went wrong"), 77 | }, 78 | }, 79 | }, 80 | appName: testAppName, 81 | namespace: testNamespace, 82 | enableServiceCatalog: false, 83 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 84 | external, _, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 85 | assert.False(t, external, "error should be an internal error") 86 | assert.EqualError(t, err, `an error on the server ("unknown") has prevented the request from succeeding`) 87 | 88 | actions := tc.smithFake.Actions() 89 | require.Len(t, actions, 2) 90 | assert.Implements(t, (*kube_testing.ListAction)(nil), actions[0]) 91 | assert.Implements(t, (*kube_testing.WatchAction)(nil), actions[1]) 92 | }, 93 | } 94 | tc.run(t) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/deleted_bundle_manual_delete_resources_success_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | "github.com/atlassian/smith/pkg/resources" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | core_v1 "k8s.io/api/core/v1" 14 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/util/sets" 17 | kube_testing "k8s.io/client-go/testing" 18 | ) 19 | 20 | // Should manually delete all resources and remove the "deleteResources" 21 | // finalizer 22 | func TestDeleteResourcesManuallyWithoutForegroundDeletion(t *testing.T) { 23 | t.Parallel() 24 | now := meta_v1.Now() 25 | tc := testCase{ 26 | mainClientObjects: []runtime.Object{ 27 | configMapNeedsDelete(), 28 | configMapNeedsUpdate(), 29 | }, 30 | scClientObjects: []runtime.Object{ 31 | serviceInstance(false, false, true), 32 | }, 33 | bundle: &smith_v1.Bundle{ 34 | ObjectMeta: meta_v1.ObjectMeta{ 35 | Name: bundle1, 36 | Namespace: testNamespace, 37 | UID: bundle1uid, 38 | DeletionTimestamp: &now, 39 | // Finalizer to enforce manual deletion 40 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 41 | }, 42 | Spec: smith_v1.BundleSpec{ 43 | Resources: []smith_v1.Resource{ 44 | { 45 | Name: resMapNeedsAnUpdate, 46 | Spec: smith_v1.ResourceSpec{ 47 | Object: &core_v1.ConfigMap{ 48 | TypeMeta: meta_v1.TypeMeta{ 49 | Kind: "ConfigMap", 50 | APIVersion: core_v1.SchemeGroupVersion.String(), 51 | }, 52 | ObjectMeta: meta_v1.ObjectMeta{ 53 | Name: mapNeedsAnUpdate, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | expectedActions: sets.NewString( 62 | "DELETE=/api/v1/namespaces/"+testNamespace+"/configmaps/"+mapNeedsAnUpdate, 63 | "DELETE=/api/v1/namespaces/"+testNamespace+"/configmaps/"+mapNeedsDelete, 64 | ), 65 | testHandler: fakeActionHandler{ 66 | response: map[path]fakeResponse{ 67 | { 68 | method: "DELETE", 69 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsAnUpdate, 70 | }: { 71 | statusCode: http.StatusOK, 72 | }, 73 | { 74 | method: "DELETE", 75 | path: "/api/v1/namespaces/" + testNamespace + "/configmaps/" + mapNeedsDelete, 76 | }: { 77 | statusCode: http.StatusOK, 78 | }, 79 | }, 80 | }, 81 | appName: testAppName, 82 | namespace: testNamespace, 83 | enableServiceCatalog: false, 84 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 85 | tc.defaultTest(t, ctx, cntrlr) 86 | 87 | actions := tc.smithFake.Actions() 88 | require.Len(t, actions, 3) 89 | assert.Implements(t, (*kube_testing.ListAction)(nil), actions[0]) 90 | assert.Implements(t, (*kube_testing.WatchAction)(nil), actions[1]) 91 | 92 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 93 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 94 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 95 | // Make sure that the "deleteResources" finalizer was removed and 96 | // the "foregroundDeletion" finalizer is still present 97 | assert.False(t, resources.HasFinalizer(updateBundle, bundlec.FinalizerDeleteResources)) 98 | assert.Equal(t, 0, len(updateBundle.GetFinalizers())) 99 | }, 100 | } 101 | tc.run(t) 102 | } 103 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/deleted_bundle_remove_finalizer_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/atlassian/smith/pkg/controller/bundlec" 9 | "github.com/atlassian/smith/pkg/resources" 10 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | core_v1 "k8s.io/api/core/v1" 14 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | kube_testing "k8s.io/client-go/testing" 17 | ) 18 | 19 | // Should just remove the "deletedResources" finalizer if the "foregroundDeletion" 20 | // finalizer is present 21 | func TestRemoveFinalizerWhenForegroundDeletion(t *testing.T) { 22 | t.Parallel() 23 | now := meta_v1.Now() 24 | tc := testCase{ 25 | mainClientObjects: []runtime.Object{ 26 | configMapNeedsDelete(), 27 | configMapNeedsUpdate(), 28 | }, 29 | scClientObjects: []runtime.Object{ 30 | serviceInstance(false, false, true), 31 | }, 32 | bundle: &smith_v1.Bundle{ 33 | ObjectMeta: meta_v1.ObjectMeta{ 34 | Name: bundle1, 35 | Namespace: testNamespace, 36 | UID: bundle1uid, 37 | DeletionTimestamp: &now, 38 | // If both "foregroundDeletion" and "deleteResources" finalizers 39 | // are present, just remove the latter and let GC do the work 40 | Finalizers: []string{meta_v1.FinalizerDeleteDependents, bundlec.FinalizerDeleteResources}, 41 | }, 42 | Spec: smith_v1.BundleSpec{ 43 | Resources: []smith_v1.Resource{ 44 | { 45 | Name: resSi1, 46 | Spec: smith_v1.ResourceSpec{ 47 | Object: &sc_v1b1.ServiceInstance{ 48 | TypeMeta: meta_v1.TypeMeta{ 49 | Kind: "ServiceInstance", 50 | APIVersion: sc_v1b1.SchemeGroupVersion.String(), 51 | }, 52 | ObjectMeta: meta_v1.ObjectMeta{ 53 | Name: si1, 54 | }, 55 | Spec: serviceInstanceSpec, 56 | }, 57 | }, 58 | }, 59 | { 60 | Name: resMapNeedsAnUpdate, 61 | References: []smith_v1.Reference{ 62 | {Resource: resSi1}, 63 | }, 64 | Spec: smith_v1.ResourceSpec{ 65 | Object: &core_v1.ConfigMap{ 66 | TypeMeta: meta_v1.TypeMeta{ 67 | Kind: "ConfigMap", 68 | APIVersion: core_v1.SchemeGroupVersion.String(), 69 | }, 70 | ObjectMeta: meta_v1.ObjectMeta{ 71 | Name: mapNeedsAnUpdate, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | appName: testAppName, 80 | namespace: testNamespace, 81 | enableServiceCatalog: false, 82 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 83 | tc.defaultTest(t, ctx, cntrlr) 84 | 85 | actions := tc.smithFake.Actions() 86 | require.Len(t, actions, 3) 87 | assert.Implements(t, (*kube_testing.ListAction)(nil), actions[0]) 88 | assert.Implements(t, (*kube_testing.WatchAction)(nil), actions[1]) 89 | 90 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 91 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 92 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 93 | // Make sure that the "deleteResources" finalizer was removed and 94 | // the "foregroundDeletion" finalizer is still present 95 | assert.False(t, resources.HasFinalizer(updateBundle, bundlec.FinalizerDeleteResources)) 96 | assert.True(t, resources.HasFinalizer(updateBundle, meta_v1.FinalizerDeleteDependents)) 97 | }, 98 | } 99 | tc.run(t) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/finalizer_added_if_not_present_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/atlassian/smith/pkg/controller/bundlec" 9 | "github.com/atlassian/smith/pkg/resources" 10 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | core_v1 "k8s.io/api/core/v1" 14 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | kube_testing "k8s.io/client-go/testing" 17 | ) 18 | 19 | // Should add a "deleteResources" finalizer if it's missing 20 | func TestFinalizerAdded(t *testing.T) { 21 | t.Parallel() 22 | m1 := configMapNeedsDelete() 23 | tc := testCase{ 24 | mainClientObjects: []runtime.Object{ 25 | m1, 26 | configMapNeedsUpdate(), 27 | }, 28 | scClientObjects: []runtime.Object{ 29 | serviceInstance(false, false, true), 30 | }, 31 | bundle: &smith_v1.Bundle{ 32 | ObjectMeta: meta_v1.ObjectMeta{ 33 | Name: bundle1, 34 | Namespace: testNamespace, 35 | UID: bundle1uid, 36 | }, 37 | Spec: smith_v1.BundleSpec{ 38 | Resources: []smith_v1.Resource{ 39 | { 40 | Name: resSi1, 41 | Spec: smith_v1.ResourceSpec{ 42 | Object: &sc_v1b1.ServiceInstance{ 43 | TypeMeta: meta_v1.TypeMeta{ 44 | Kind: "ServiceInstance", 45 | APIVersion: sc_v1b1.SchemeGroupVersion.String(), 46 | }, 47 | ObjectMeta: meta_v1.ObjectMeta{ 48 | Name: si1, 49 | }, 50 | Spec: serviceInstanceSpec, 51 | }, 52 | }, 53 | }, 54 | { 55 | Name: resMapNeedsAnUpdate, 56 | References: []smith_v1.Reference{ 57 | {Resource: resSi1}, 58 | }, 59 | Spec: smith_v1.ResourceSpec{ 60 | Object: &core_v1.ConfigMap{ 61 | TypeMeta: meta_v1.TypeMeta{ 62 | Kind: "ConfigMap", 63 | APIVersion: core_v1.SchemeGroupVersion.String(), 64 | }, 65 | ObjectMeta: meta_v1.ObjectMeta{ 66 | Name: mapNeedsAnUpdate, 67 | }, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | appName: testAppName, 75 | namespace: testNamespace, 76 | enableServiceCatalog: false, 77 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 78 | tc.defaultTest(t, ctx, cntrlr) 79 | 80 | actions := tc.smithFake.Actions() 81 | require.Len(t, actions, 3) 82 | assert.Implements(t, (*kube_testing.ListAction)(nil), actions[0]) 83 | assert.Implements(t, (*kube_testing.WatchAction)(nil), actions[1]) 84 | 85 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 86 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 87 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 88 | // Make sure that the "deleteResources" finalizer was added 89 | assert.True(t, resources.HasFinalizer(updateBundle, bundlec.FinalizerDeleteResources)) 90 | }, 91 | } 92 | tc.run(t) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/invalid_depends_on_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | smith_testing "github.com/atlassian/smith/pkg/util/testing" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | kube_testing "k8s.io/client-go/testing" 15 | ) 16 | 17 | // Should detect invalid references 18 | func TestInvalidReferences(t *testing.T) { 19 | t.Parallel() 20 | tc := testCase{ 21 | bundle: &smith_v1.Bundle{ 22 | ObjectMeta: meta_v1.ObjectMeta{ 23 | Name: bundle1, 24 | Namespace: testNamespace, 25 | UID: bundle1uid, 26 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 27 | }, 28 | Spec: smith_v1.BundleSpec{ 29 | Resources: []smith_v1.Resource{ 30 | { 31 | Name: resP1, 32 | References: []smith_v1.Reference{ 33 | {Resource: smith_v1.ResourceName("bla")}, 34 | }, 35 | Spec: smith_v1.ResourceSpec{ 36 | Plugin: &smith_v1.PluginSpec{ 37 | Name: pluginConfigMapWithDeps, 38 | ObjectName: m1, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | appName: testAppName, 46 | namespace: testNamespace, 47 | plugins: map[smith_v1.PluginName]func(*testing.T) testingPlugin{ 48 | pluginConfigMapWithDeps: configMapWithDependenciesPlugin(false, false), 49 | }, 50 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 51 | external, retriable, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 52 | assert.EqualError(t, err, `topological sort of resources failed: vertex "bla" not found`) 53 | assert.True(t, external, "error should be an external error") 54 | assert.False(t, retriable, "error should not be retriable") 55 | 56 | actions := tc.smithFake.Actions() 57 | require.Len(t, actions, 3) 58 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 59 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 60 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 61 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleReady, cond_v1.ConditionFalse) 62 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleInProgress, cond_v1.ConditionFalse) 63 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleError, cond_v1.ConditionTrue) 64 | 65 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceBlocked, cond_v1.ConditionUnknown) 66 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceInProgress, cond_v1.ConditionUnknown) 67 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceReady, cond_v1.ConditionUnknown) 68 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceError, cond_v1.ConditionUnknown) 69 | }, 70 | } 71 | tc.run(t) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/no_deletions_while_in_progress_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/atlassian/smith/pkg/controller/bundlec" 9 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 10 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | ) 13 | 14 | // Should not delete resources while Bundle processing is in progress 15 | func TestNoDeletionsWhileInProgress(t *testing.T) { 16 | t.Parallel() 17 | m1 := configMapNeedsDelete() 18 | m2 := configMapMarkedForDeletion() 19 | tc := testCase{ 20 | mainClientObjects: []runtime.Object{ 21 | m1, 22 | m2, 23 | }, 24 | scClientObjects: []runtime.Object{ 25 | serviceInstance(false, true, false), 26 | }, 27 | bundle: &smith_v1.Bundle{ 28 | ObjectMeta: meta_v1.ObjectMeta{ 29 | Name: bundle1, 30 | Namespace: testNamespace, 31 | UID: bundle1uid, 32 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 33 | }, 34 | Spec: smith_v1.BundleSpec{ 35 | Resources: []smith_v1.Resource{ 36 | { 37 | Name: resSi1, 38 | Spec: smith_v1.ResourceSpec{ 39 | Object: &sc_v1b1.ServiceInstance{ 40 | TypeMeta: meta_v1.TypeMeta{ 41 | Kind: "ServiceInstance", 42 | APIVersion: sc_v1b1.SchemeGroupVersion.String(), 43 | }, 44 | ObjectMeta: meta_v1.ObjectMeta{ 45 | Name: si1, 46 | }, 47 | Spec: serviceInstanceSpec, 48 | }, 49 | }, 50 | }, 51 | }, 52 | }, 53 | }, 54 | appName: testAppName, 55 | namespace: testNamespace, 56 | enableServiceCatalog: true, 57 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 58 | tc.defaultTest(t, ctx, cntrlr) 59 | tc.assertObjectsToBeDeleted(t, m2, m1) 60 | }, 61 | } 62 | tc.run(t) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/not_marked_crd_ignored_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/atlassian/smith" 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/atlassian/smith/pkg/controller/bundlec" 9 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | // Should not setup watch for a CRD that does not have smith.CrdSupportEnabled annotation set to true 14 | func TestNotMarkedCrdIgnored(t *testing.T) { 15 | t.Parallel() 16 | t.Run("set to false", func(t *testing.T) { 17 | t.Parallel() 18 | sleeperCrd := sleeperCrdWithStatus() 19 | sleeperCrd.Annotations[smith.CrdSupportEnabled] = "false" 20 | tc := testCase{ 21 | apiExtClientObjects: []runtime.Object{ 22 | sleeperCrd, 23 | }, 24 | bundle: &smith_v1.Bundle{ 25 | ObjectMeta: meta_v1.ObjectMeta{ 26 | Name: bundle1, 27 | Namespace: testNamespace, 28 | UID: bundle1uid, 29 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 30 | }, 31 | }, 32 | appName: testAppName, 33 | namespace: testNamespace, 34 | } 35 | tc.run(t) 36 | }) 37 | t.Run("not set", func(t *testing.T) { 38 | t.Parallel() 39 | sleeperCrd := sleeperCrdWithStatus() 40 | delete(sleeperCrd.Annotations, smith.CrdSupportEnabled) 41 | tc := testCase{ 42 | apiExtClientObjects: []runtime.Object{ 43 | sleeperCrd, 44 | }, 45 | bundle: &smith_v1.Bundle{ 46 | ObjectMeta: meta_v1.ObjectMeta{ 47 | Name: bundle1, 48 | Namespace: testNamespace, 49 | UID: bundle1uid, 50 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 51 | }, 52 | }, 53 | appName: testAppName, 54 | namespace: testNamespace, 55 | } 56 | tc.run(t) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/plugin_error_propagated_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | smith_testing "github.com/atlassian/smith/pkg/util/testing" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/util/sets" 15 | kube_testing "k8s.io/client-go/testing" 16 | ) 17 | 18 | // Should propagate plugin error into resource's status 19 | func TestPluginErrorPropagated(t *testing.T) { 20 | t.Parallel() 21 | tc := testCase{ 22 | bundle: &smith_v1.Bundle{ 23 | ObjectMeta: meta_v1.ObjectMeta{ 24 | Name: bundle1, 25 | Namespace: testNamespace, 26 | UID: bundle1uid, 27 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 28 | }, 29 | Spec: smith_v1.BundleSpec{ 30 | Resources: []smith_v1.Resource{ 31 | { 32 | Name: resP1, 33 | Spec: smith_v1.ResourceSpec{ 34 | Plugin: &smith_v1.PluginSpec{ 35 | Name: pluginFailing, 36 | ObjectName: m1, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | plugins: map[smith_v1.PluginName]func(*testing.T) testingPlugin{ 44 | pluginFailing: newFailingPlugin, 45 | }, 46 | pluginsShouldBeInvoked: sets.NewString(string(pluginFailing)), 47 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 48 | external, retriable, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 49 | assert.EqualError(t, err, `error processing resource(s): ["`+resP1+`"]`) 50 | assert.False(t, external, "error should be an internal error") // 'failingPlugin' returns internal err 51 | assert.False(t, retriable, "error should not be a retriable error") 52 | 53 | actions := tc.smithFake.Actions() 54 | require.Len(t, actions, 3) 55 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 56 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 57 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 58 | 59 | resCond := smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceError, cond_v1.ConditionTrue) 60 | if resCond != nil { 61 | assert.Equal(t, smith_v1.ResourceReasonTerminalError, resCond.Reason) 62 | assert.Equal(t, "plugin failed as it should. BOOM!", resCond.Message) 63 | } 64 | }, 65 | } 66 | tc.run(t) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/plugin_schema_invalid_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | smith_testing "github.com/atlassian/smith/pkg/util/testing" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | kube_testing "k8s.io/client-go/testing" 15 | ) 16 | 17 | // Should not process plugin if specification is invalid according to the schema 18 | func TestPluginSchemaInvalid(t *testing.T) { 19 | t.Parallel() 20 | tc := testCase{ 21 | bundle: &smith_v1.Bundle{ 22 | ObjectMeta: meta_v1.ObjectMeta{ 23 | Name: bundle1, 24 | Namespace: testNamespace, 25 | UID: bundle1uid, 26 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 27 | }, 28 | Spec: smith_v1.BundleSpec{ 29 | Resources: []smith_v1.Resource{ 30 | { 31 | Name: resP1, 32 | Spec: smith_v1.ResourceSpec{ 33 | Plugin: &smith_v1.PluginSpec{ 34 | Name: pluginConfigMapWithDeps, 35 | ObjectName: m1, 36 | Spec: map[string]interface{}{ 37 | "p1": nil, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | plugins: map[smith_v1.PluginName]func(*testing.T) testingPlugin{ 46 | pluginConfigMapWithDeps: configMapWithDependenciesPlugin(false, false), 47 | }, 48 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 49 | external, retriable, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 50 | assert.EqualError(t, err, `error processing resource(s): ["`+resP1+`"]`) 51 | assert.True(t, external, "error should be an external error") 52 | assert.False(t, retriable, "error should not be retriable error") 53 | 54 | actions := tc.smithFake.Actions() 55 | require.Len(t, actions, 3) 56 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 57 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 58 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 59 | 60 | resCond := smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceError, cond_v1.ConditionTrue) 61 | if resCond != nil { 62 | assert.Equal(t, smith_v1.ResourceReasonTerminalError, resCond.Reason) 63 | assert.Equal(t, "spec failed validation against schema: p1: Invalid type. Expected: string, given: null", resCond.Message) 64 | } 65 | }, 66 | } 67 | tc.run(t) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/prohibited_annotations_object_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 8 | "github.com/atlassian/smith" 9 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 10 | "github.com/atlassian/smith/pkg/controller/bundlec" 11 | smith_testing "github.com/atlassian/smith/pkg/util/testing" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | core_v1 "k8s.io/api/core/v1" 15 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | kube_testing "k8s.io/client-go/testing" 17 | ) 18 | 19 | // Should detect prohibited annotations in resource spec and return error 20 | func TestProhibitedAnnotationsObjectRejected(t *testing.T) { 21 | t.Parallel() 22 | 23 | r1 := smith_v1.ResourceName("resource1") 24 | cm1 := &core_v1.ConfigMap{ 25 | TypeMeta: meta_v1.TypeMeta{ 26 | Kind: "ConfigMap", 27 | APIVersion: core_v1.SchemeGroupVersion.String(), 28 | }, 29 | ObjectMeta: meta_v1.ObjectMeta{ 30 | Name: mapNeedsAnUpdate, 31 | Annotations: map[string]string{ 32 | smith.DeletionTimestampAnnotation: "2006-01-02T15:04:05Z07:00", 33 | }, 34 | }, 35 | Data: map[string]string{ 36 | "delete": "this key", 37 | }, 38 | } 39 | 40 | tc := testCase{ 41 | bundle: &smith_v1.Bundle{ 42 | ObjectMeta: meta_v1.ObjectMeta{ 43 | Name: bundle1, 44 | Namespace: testNamespace, 45 | UID: bundle1uid, 46 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 47 | }, 48 | Spec: smith_v1.BundleSpec{ 49 | Resources: []smith_v1.Resource{ 50 | { 51 | Name: r1, 52 | Spec: smith_v1.ResourceSpec{ 53 | Object: cm1, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | appName: testAppName, 60 | namespace: testNamespace, 61 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 62 | external, retriable, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 63 | assert.EqualError(t, err, `error processing resource(s): ["`+string(r1)+`"]`) 64 | assert.True(t, external, "error should be an external error") // user tried to set annotation 65 | assert.False(t, retriable, "error should not be a retriable error") 66 | 67 | actions := tc.smithFake.Actions() 68 | require.Len(t, actions, 3) 69 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 70 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 71 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 72 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleReady, cond_v1.ConditionFalse) 73 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleInProgress, cond_v1.ConditionFalse) 74 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleError, cond_v1.ConditionTrue) 75 | 76 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceBlocked, cond_v1.ConditionFalse) 77 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceInProgress, cond_v1.ConditionFalse) 78 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceReady, cond_v1.ConditionFalse) 79 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceError, cond_v1.ConditionTrue) 80 | 81 | smith_testing.AssertResourceConditionMessage(t, updateBundle, r1, smith_v1.ResourceError, `annotation "smith.atlassian.com/deletionTimestamp" cannot be set by the user`) 82 | }, 83 | } 84 | tc.run(t) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/prohibited_annotations_plugin_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 8 | "github.com/atlassian/smith" 9 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 10 | "github.com/atlassian/smith/pkg/controller/bundlec" 11 | smith_testing "github.com/atlassian/smith/pkg/util/testing" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | core_v1 "k8s.io/api/core/v1" 15 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/util/sets" 17 | kube_testing "k8s.io/client-go/testing" 18 | ) 19 | 20 | // Should detect prohibited annotations in resource spec returned by a plugin and return error 21 | func TestProhibitedAnnotationsPluginRejected(t *testing.T) { 22 | t.Parallel() 23 | 24 | r1 := smith_v1.ResourceName("resource1") 25 | cm1 := &core_v1.ConfigMap{ 26 | TypeMeta: meta_v1.TypeMeta{ 27 | Kind: "ConfigMap", 28 | APIVersion: core_v1.SchemeGroupVersion.String(), 29 | }, 30 | ObjectMeta: meta_v1.ObjectMeta{ 31 | Name: mapNeedsAnUpdate, 32 | Annotations: map[string]string{ 33 | smith.DeletionTimestampAnnotation: "2006-01-02T15:04:05Z07:00", 34 | }, 35 | }, 36 | Data: map[string]string{ 37 | "delete": "this key", 38 | }, 39 | } 40 | 41 | tc := testCase{ 42 | bundle: &smith_v1.Bundle{ 43 | ObjectMeta: meta_v1.ObjectMeta{ 44 | Name: bundle1, 45 | Namespace: testNamespace, 46 | UID: bundle1uid, 47 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 48 | }, 49 | Spec: smith_v1.BundleSpec{ 50 | Resources: []smith_v1.Resource{ 51 | { 52 | Name: r1, 53 | Spec: smith_v1.ResourceSpec{ 54 | Plugin: &smith_v1.PluginSpec{ 55 | Name: pluginMockConfigMap, 56 | ObjectName: string(r1), 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | appName: testAppName, 64 | namespace: testNamespace, 65 | plugins: map[smith_v1.PluginName]func(*testing.T) testingPlugin{ 66 | pluginMockConfigMap: mockConfigMapPlugin(cm1), 67 | }, 68 | pluginsShouldBeInvoked: sets.NewString(string(pluginMockConfigMap)), 69 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 70 | external, retriable, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 71 | assert.EqualError(t, err, `error processing resource(s): ["`+string(r1)+`"]`) 72 | assert.True(t, external, "error should be an external error") // annotation should not be set 73 | assert.False(t, retriable, "error should not be a retriable error") 74 | 75 | actions := tc.smithFake.Actions() 76 | require.Len(t, actions, 3) 77 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 78 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 79 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 80 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleReady, cond_v1.ConditionFalse) 81 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleInProgress, cond_v1.ConditionFalse) 82 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleError, cond_v1.ConditionTrue) 83 | 84 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceBlocked, cond_v1.ConditionFalse) 85 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceInProgress, cond_v1.ConditionFalse) 86 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceReady, cond_v1.ConditionFalse) 87 | smith_testing.AssertResourceCondition(t, updateBundle, r1, smith_v1.ResourceError, cond_v1.ConditionTrue) 88 | 89 | smith_testing.AssertResourceConditionMessage(t, updateBundle, r1, smith_v1.ResourceError, `annotation "smith.atlassian.com/deletionTimestamp" cannot be set by the user`) 90 | }, 91 | } 92 | tc.run(t) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/propagate_status_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | smith_testing "github.com/atlassian/smith/pkg/util/testing" 11 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | ) 17 | 18 | // Should correctly propagate status information 19 | func TestStatusPropagated(t *testing.T) { 20 | t.Parallel() 21 | 22 | si := serviceInstance(false, false, false) 23 | si.Status.Conditions = append(si.Status.Conditions, sc_v1b1.ServiceInstanceCondition{ 24 | Type: sc_v1b1.ServiceInstanceConditionReady, 25 | Status: sc_v1b1.ConditionFalse, 26 | Reason: "ProvisionCallFailed", 27 | Message: "Error provisioning ServiceInstance of ClusterServiceClass failed", 28 | }) 29 | 30 | tc := testCase{ 31 | scClientObjects: []runtime.Object{si}, 32 | appName: testAppName, 33 | namespace: testNamespace, 34 | enableServiceCatalog: true, 35 | bundle: &smith_v1.Bundle{ 36 | ObjectMeta: meta_v1.ObjectMeta{ 37 | Name: bundle1, 38 | Namespace: testNamespace, 39 | UID: bundle1uid, 40 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 41 | }, 42 | Spec: smith_v1.BundleSpec{ 43 | Resources: []smith_v1.Resource{ 44 | { 45 | Name: resSi1, 46 | Spec: smith_v1.ResourceSpec{ 47 | Object: &sc_v1b1.ServiceInstance{ 48 | TypeMeta: meta_v1.TypeMeta{ 49 | Kind: "ServiceInstance", 50 | APIVersion: sc_v1b1.SchemeGroupVersion.String(), 51 | }, 52 | ObjectMeta: meta_v1.ObjectMeta{ 53 | Name: si1, 54 | }, 55 | Spec: serviceInstanceSpec, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 63 | require.NotNil(t, tc.bundle) 64 | external, retriable, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 65 | require.EqualError(t, err, "error processing resource(s): [\"resSi1\"]") 66 | assert.True(t, external, "error should be an external error") // service instance failures 67 | assert.True(t, retriable, "error should be a retriable error") // ProvisionCallFailed is retriable 68 | bundle := tc.findBundleUpdate(t, true) 69 | smith_testing.AssertResourceCondition(t, bundle, "resSi1", smith_v1.BundleError, cond_v1.ConditionTrue) 70 | smith_testing.AssertResourceConditionMessage(t, bundle, "resSi1", smith_v1.BundleError, "ProvisionCallFailed: Error provisioning ServiceInstance of ClusterServiceClass failed") 71 | }, 72 | } 73 | 74 | tc.run(t) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/secret_keys_not_merged_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/atlassian/smith/pkg/controller/bundlec" 9 | core_v1 "k8s.io/api/core/v1" 10 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/util/sets" 13 | ) 14 | 15 | // Should not merge Secret keys 16 | func TestSecretKeysNotMerged(t *testing.T) { 17 | tr := true 18 | t.Parallel() 19 | tc := testCase{ 20 | mainClientObjects: []runtime.Object{ 21 | &core_v1.Secret{ 22 | ObjectMeta: meta_v1.ObjectMeta{ 23 | Name: s1, 24 | Namespace: testNamespace, 25 | UID: s1uid, 26 | OwnerReferences: []meta_v1.OwnerReference{ 27 | { 28 | APIVersion: smith_v1.BundleResourceGroupVersion, 29 | Kind: smith_v1.BundleResourceKind, 30 | Name: bundle1, 31 | UID: bundle1uid, 32 | Controller: &tr, 33 | BlockOwnerDeletion: &tr, 34 | }, 35 | }, 36 | }, 37 | Data: map[string][]byte{ 38 | "data_actual": []byte("bla0"), 39 | }, 40 | Type: core_v1.SecretTypeOpaque, 41 | }, 42 | }, 43 | bundle: &smith_v1.Bundle{ 44 | ObjectMeta: meta_v1.ObjectMeta{ 45 | Name: bundle1, 46 | Namespace: testNamespace, 47 | UID: bundle1uid, 48 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 49 | }, 50 | Spec: smith_v1.BundleSpec{ 51 | Resources: []smith_v1.Resource{ 52 | { 53 | Name: resSi1, 54 | Spec: smith_v1.ResourceSpec{ 55 | Object: &core_v1.Secret{ 56 | TypeMeta: meta_v1.TypeMeta{ 57 | Kind: "Secret", 58 | APIVersion: core_v1.SchemeGroupVersion.String(), 59 | }, 60 | ObjectMeta: meta_v1.ObjectMeta{ 61 | Name: s1, 62 | }, 63 | StringData: map[string]string{ 64 | "data_spec2": "bla2", 65 | }, 66 | Type: core_v1.SecretTypeOpaque, 67 | }, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | appName: testAppName, 74 | namespace: testNamespace, 75 | expectedActions: sets.NewString("PUT=/api/v1/namespaces/" + testNamespace + "/secrets/" + s1), 76 | testHandler: fakeActionHandler{ 77 | response: map[path]fakeResponse{ 78 | { 79 | method: "PUT", 80 | path: "/api/v1/namespaces/" + testNamespace + "/secrets/" + s1, 81 | }: { 82 | statusCode: http.StatusOK, 83 | content: []byte(`{ 84 | "apiVersion": "v1", 85 | "kind": "Secret", 86 | "metadata": { 87 | "name": "` + s1 + `", 88 | "namespace": "` + testNamespace + `", 89 | "uid": "` + s1uid + `", 90 | "creationTimestamp": null, 91 | "ownerReferences": [ 92 | { 93 | "apiVersion": "` + smith_v1.BundleResourceGroupVersion + `", 94 | "kind": "` + smith_v1.BundleResourceKind + `", 95 | "name": "` + bundle1 + `", 96 | "uid": "` + bundle1uid + `", 97 | "controller": true, 98 | "blockOwnerDeletion": true 99 | } 100 | ] 101 | }, 102 | "data": { 103 | "data_spec2": "YmxhMg==" 104 | }, 105 | "type": "Opaque" 106 | }`), 107 | }, 108 | }, 109 | }, 110 | } 111 | tc.run(t) 112 | } 113 | -------------------------------------------------------------------------------- /pkg/controller/bundlec_test/two_resources_same_name_test.go: -------------------------------------------------------------------------------- 1 | package bundlec_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "github.com/atlassian/smith/pkg/controller/bundlec" 10 | smith_testing "github.com/atlassian/smith/pkg/util/testing" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | kube_testing "k8s.io/client-go/testing" 15 | ) 16 | 17 | // Should detect two resources with the same name 18 | func TestTwoResourcesWithSameName(t *testing.T) { 19 | t.Parallel() 20 | tc := testCase{ 21 | bundle: &smith_v1.Bundle{ 22 | ObjectMeta: meta_v1.ObjectMeta{ 23 | Name: bundle1, 24 | Namespace: testNamespace, 25 | UID: bundle1uid, 26 | Finalizers: []string{bundlec.FinalizerDeleteResources}, 27 | }, 28 | Spec: smith_v1.BundleSpec{ 29 | Resources: []smith_v1.Resource{ 30 | { 31 | Name: resP1, 32 | Spec: smith_v1.ResourceSpec{ 33 | Plugin: &smith_v1.PluginSpec{ 34 | Name: pluginConfigMapWithDeps, 35 | ObjectName: m1, 36 | }, 37 | }, 38 | }, 39 | { 40 | Name: resP1, 41 | Spec: smith_v1.ResourceSpec{ 42 | Plugin: &smith_v1.PluginSpec{ 43 | Name: pluginConfigMapWithDeps, 44 | ObjectName: m1, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | appName: testAppName, 52 | namespace: testNamespace, 53 | plugins: map[smith_v1.PluginName]func(*testing.T) testingPlugin{ 54 | pluginConfigMapWithDeps: configMapWithDependenciesPlugin(false, false), 55 | }, 56 | test: func(t *testing.T, ctx context.Context, cntrlr *bundlec.Controller, tc *testCase) { 57 | external, retriable, err := cntrlr.ProcessBundle(tc.logger, tc.bundle) 58 | assert.EqualError(t, err, `bundle contains two resources with the same name "`+resP1+`"`) 59 | assert.True(t, external, "error should be an external error") // resources with same name 60 | assert.False(t, retriable, "error should not be a retriable error") 61 | 62 | actions := tc.smithFake.Actions() 63 | require.Len(t, actions, 3) 64 | bundleUpdate := actions[2].(kube_testing.UpdateAction) 65 | assert.Equal(t, testNamespace, bundleUpdate.GetNamespace()) 66 | updateBundle := bundleUpdate.GetObject().(*smith_v1.Bundle) 67 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleReady, cond_v1.ConditionFalse) 68 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleInProgress, cond_v1.ConditionFalse) 69 | smith_testing.AssertCondition(t, updateBundle, smith_v1.BundleError, cond_v1.ConditionTrue) 70 | 71 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceBlocked, cond_v1.ConditionUnknown) 72 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceInProgress, cond_v1.ConditionUnknown) 73 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceReady, cond_v1.ConditionUnknown) 74 | smith_testing.AssertResourceCondition(t, updateBundle, resP1, smith_v1.ResourceError, cond_v1.ConditionUnknown) 75 | }, 76 | } 77 | tc.run(t) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/crd/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["crd.go"], 6 | importpath = "github.com/atlassian/smith/pkg/crd", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//pkg/apis/smith:go_default_library", 10 | "//pkg/apis/smith/v1:go_default_library", 11 | "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", 12 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/plugin/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "plugin.go", 7 | "types.go", 8 | ], 9 | importpath = "github.com/atlassian/smith/pkg/plugin", 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "//pkg/apis/smith/v1:go_default_library", 13 | "//vendor/github.com/pkg/errors:go_default_library", 14 | "//vendor/github.com/xeipuuv/gojsonschema:go_default_library", 15 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 16 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/xeipuuv/gojsonschema" 6 | ) 7 | 8 | type Container struct { 9 | Plugin Plugin 10 | schema *gojsonschema.Schema 11 | } 12 | 13 | type ValidationResult struct { 14 | Errors []error 15 | } 16 | 17 | func NewContainer(newPlugin NewFunc) (Container, error) { 18 | plugin, err := newPlugin() 19 | if err != nil { 20 | return Container{}, errors.Wrap(err, "failed to instantiate plugin") 21 | } 22 | description := plugin.Describe() 23 | var schema *gojsonschema.Schema 24 | if description.SpecSchema != nil { 25 | schema, err = gojsonschema.NewSchema(gojsonschema.NewBytesLoader(description.SpecSchema)) 26 | if err != nil { 27 | return Container{}, errors.Wrapf(err, "can't use plugin %q due to invalid schema", description.Name) 28 | } 29 | } 30 | 31 | return Container{ 32 | Plugin: plugin, 33 | schema: schema, 34 | }, nil 35 | } 36 | 37 | func (pc *Container) ValidateSpec(pluginSpec map[string]interface{}) (ValidationResult, error) { 38 | if pc.schema == nil { 39 | return ValidationResult{}, nil 40 | } 41 | 42 | result, err := pc.schema.Validate(gojsonschema.NewGoLoader(pluginSpec)) 43 | if err != nil { 44 | return ValidationResult{}, errors.Wrap(err, "error validating plugin spec") 45 | } 46 | 47 | if !result.Valid() { 48 | validationErrors := result.Errors() 49 | errs := make([]error, 0, len(validationErrors)) 50 | 51 | for _, validationErr := range validationErrors { 52 | errs = append(errs, errors.New(validationErr.String())) 53 | } 54 | 55 | return ValidationResult{errs}, nil 56 | } 57 | 58 | return ValidationResult{}, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/plugin/types.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | // TODO refactor so we have more detail on plugin? 4 | // (i.e. name, output types, so we can fail early if bundle is incorrect) 5 | // OR have plugin validate itself 6 | 7 | import ( 8 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | ) 12 | 13 | type ProcessResultType string 14 | 15 | const ( 16 | ProcessResultSuccessType ProcessResultType = "Success" 17 | ProcessResultFailureType ProcessResultType = "Failure" 18 | ) 19 | 20 | // NewFunc is a factory function that returns an initialized plugin. 21 | // Called once on Smith startup. 22 | type NewFunc func() (Plugin, error) 23 | 24 | // Plugin represents a plugin and the functionality it provides. 25 | type Plugin interface { 26 | // Describe returns information about the plugin. 27 | Describe() *Description 28 | // Process processes a plugin specification and produces an object as the result. 29 | Process(map[string]interface{}, *Context) ProcessResult 30 | } 31 | 32 | type Description struct { 33 | Name smith_v1.PluginName 34 | GVK schema.GroupVersionKind 35 | // gojsonschema supported schema for the spec (first argument of Process) 36 | SpecSchema []byte 37 | } 38 | 39 | // Context contains contextual information for the Process() call. 40 | type Context struct { 41 | // Namespace is the namespace where the returned object will be created. 42 | Namespace string 43 | // Actual is the actual object that will be updated if it exists already. 44 | // nil if the object does not exist. 45 | Actual runtime.Object 46 | // Dependencies is the map from dependency name to a description of that dependency. 47 | Dependencies map[smith_v1.ResourceName]Dependency 48 | } 49 | 50 | // Dependency contains information about a dependency of a resource that a plugin is processing. 51 | type Dependency struct { 52 | // Spec is the specification of the resource as specified in the Bundle. 53 | Spec smith_v1.Resource 54 | // Actual is the actual dependency object. 55 | Actual runtime.Object 56 | // Outputs are objects produced by the actual object. 57 | Outputs []runtime.Object 58 | // Auxiliary are objects that somehow relate to the actual object. 59 | Auxiliary []runtime.Object 60 | } 61 | 62 | // ProcessResult contains result of the Process() call. 63 | type ProcessResult interface { 64 | StatusType() ProcessResultType 65 | } 66 | 67 | type ProcessResultSuccess struct { 68 | // Object is the object that should be created/updated. 69 | Object runtime.Object 70 | } 71 | 72 | type ProcessResultFailure struct { 73 | Error error 74 | IsExternalError bool 75 | IsRetriableError bool 76 | } 77 | 78 | func (r *ProcessResultSuccess) StatusType() ProcessResultType { 79 | return ProcessResultSuccessType 80 | } 81 | 82 | func (r *ProcessResultFailure) StatusType() ProcessResultType { 83 | return ProcessResultFailureType 84 | } 85 | -------------------------------------------------------------------------------- /pkg/resources/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "crd_helpers.go", 7 | "objects.go", 8 | ], 9 | importpath = "github.com/atlassian/smith/pkg/resources", 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "//pkg/util:go_default_library", 13 | "//vendor/github.com/atlassian/ctrl/logz:go_default_library", 14 | "//vendor/github.com/pkg/errors:go_default_library", 15 | "//vendor/go.uber.org/zap:go_default_library", 16 | "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", 17 | "//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", 18 | "//vendor/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1beta1:go_default_library", 19 | "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", 20 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 21 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 22 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 23 | "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", 24 | "//vendor/k8s.io/client-go/util/jsonpath:go_default_library", 25 | "//vendor/sigs.k8s.io/yaml:go_default_library", 26 | ], 27 | ) 28 | 29 | go_test( 30 | name = "go_default_test", 31 | size = "small", 32 | srcs = ["objects_test.go"], 33 | embed = [":go_default_library"], 34 | race = "on", 35 | deps = [ 36 | "//pkg/apis/smith/v1:go_default_library", 37 | "//vendor/github.com/atlassian/ctrl/apis/condition/v1:go_default_library", 38 | "//vendor/github.com/stretchr/testify/assert:go_default_library", 39 | "//vendor/github.com/stretchr/testify/require:go_default_library", 40 | "//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /pkg/resources/objects.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/pkg/errors" 7 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/util/jsonpath" 9 | ) 10 | 11 | // GetJSONPathString extracts the value from the object using given JsonPath template, in a string format 12 | func GetJSONPathString(obj interface{}, path string) (string, error) { 13 | j := jsonpath.New("GetJSONPathString") 14 | // If the key is missing, return an empty string without errors 15 | j.AllowMissingKeys(true) 16 | err := j.Parse(path) 17 | if err != nil { 18 | return "", errors.Wrapf(err, "JsonPath parse %s error", path) 19 | } 20 | var buf bytes.Buffer 21 | err = j.Execute(&buf, obj) 22 | if err != nil { 23 | return "", errors.Wrap(err, "JsonPath execute error") 24 | } 25 | return buf.String(), nil 26 | } 27 | 28 | // GetJSONPathValue extracts the value from the object using given JsonPath template 29 | func GetJSONPathValue(obj interface{}, path string, allowMissingKeys bool) (interface{}, error) { 30 | j := jsonpath.New("GetJSONPathValue") 31 | // If the key is missing, return an empty string without errors 32 | j.AllowMissingKeys(allowMissingKeys) 33 | err := j.Parse(path) 34 | if err != nil { 35 | return "", errors.Wrapf(err, "JsonPath parse %s error", path) 36 | } 37 | values, err := j.FindResults(obj) 38 | if err != nil { 39 | return "", errors.Wrap(err, "JsonPath execute error") 40 | } 41 | if len(values) == 0 { 42 | return nil, nil 43 | } 44 | if len(values) > 1 { 45 | return nil, errors.Errorf("single result expected, got %d", len(values)) 46 | } 47 | if values[0] == nil || len(values[0]) == 0 || values[0][0].IsNil() { 48 | return nil, nil 49 | } 50 | return values[0][0].Interface(), nil 51 | } 52 | 53 | func HasFinalizer(accessor meta_v1.Object, finalizer string) bool { 54 | finalizers := accessor.GetFinalizers() 55 | for _, f := range finalizers { 56 | if f == finalizer { 57 | return true 58 | } 59 | } 60 | return false 61 | } 62 | -------------------------------------------------------------------------------- /pkg/resources/objects_test.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "testing" 5 | 6 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "k8s.io/apimachinery/pkg/util/json" 11 | ) 12 | 13 | func TestGetJsonPathStringBundle(t *testing.T) { 14 | t.Parallel() 15 | b := &smith_v1.Bundle{ 16 | Status: smith_v1.BundleStatus{ 17 | Conditions: []cond_v1.Condition{ 18 | { 19 | Type: smith_v1.BundleError, 20 | Status: cond_v1.ConditionFalse, 21 | }, 22 | { 23 | Type: smith_v1.BundleReady, 24 | Status: cond_v1.ConditionTrue, 25 | }, 26 | { 27 | Type: smith_v1.BundleInProgress, 28 | Status: cond_v1.ConditionFalse, 29 | }, 30 | }, 31 | }, 32 | } 33 | bytes, err := json.Marshal(b) 34 | require.NoError(t, err) 35 | unstructured := make(map[string]interface{}) 36 | err = json.Unmarshal(bytes, &unstructured) 37 | require.NoError(t, err) 38 | status, err := GetJSONPathString(unstructured, `{$.status.conditions[?(@.type=="Ready")].status}`) 39 | require.NoError(t, err) 40 | assert.Equal(t, string(cond_v1.ConditionTrue), status) 41 | } 42 | 43 | func TestGetJsonPathStringMissing(t *testing.T) { 44 | t.Parallel() 45 | // Bundle with empty status 46 | b := &smith_v1.Bundle{} 47 | bytes, err := json.Marshal(b) 48 | require.NoError(t, err) 49 | unstructured := make(map[string]interface{}) 50 | err = json.Unmarshal(bytes, &unstructured) 51 | require.NoError(t, err) 52 | status, err := GetJSONPathString(unstructured, `{$.status.conditions[?(@.type=="Ready")].status}`) 53 | // Should return empty string without errors 54 | require.NoError(t, err) 55 | require.Equal(t, "", status) 56 | } 57 | 58 | func TestGetJsonPathStringInvalid(t *testing.T) { 59 | t.Parallel() 60 | b := &smith_v1.Bundle{ 61 | Status: smith_v1.BundleStatus{ 62 | Conditions: []cond_v1.Condition{ 63 | { 64 | Type: smith_v1.BundleReady, 65 | Status: cond_v1.ConditionTrue, 66 | }, 67 | }, 68 | }, 69 | } 70 | bytes, err := json.Marshal(b) 71 | require.NoError(t, err) 72 | unstructured := make(map[string]interface{}) 73 | err = json.Unmarshal(bytes, &unstructured) 74 | require.NoError(t, err) 75 | // Invalid JsonPath format: missing quotes around "Ready" 76 | _, err = GetJSONPathString(unstructured, `{$.status.conditions[?(@.type==Ready)].status}`) 77 | require.EqualError(t, err, "JsonPath execute error: unrecognized identifier Ready") 78 | } 79 | -------------------------------------------------------------------------------- /pkg/specchecker/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "checker.go", 7 | "hash.go", 8 | "types.go", 9 | ], 10 | importpath = "github.com/atlassian/smith/pkg/specchecker", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//pkg/util:go_default_library", 14 | "//vendor/github.com/pkg/errors:go_default_library", 15 | "//vendor/go.uber.org/zap:go_default_library", 16 | "//vendor/k8s.io/api/core/v1:go_default_library", 17 | "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", 18 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 19 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 20 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 21 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 22 | "//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library", 23 | "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", 24 | ], 25 | ) 26 | 27 | go_test( 28 | name = "go_default_test", 29 | size = "small", 30 | srcs = ["checker_test.go"], 31 | embed = [":go_default_library"], 32 | race = "on", 33 | deps = [ 34 | "//pkg/specchecker/builtin:go_default_library", 35 | "//pkg/specchecker/testing:go_default_library", 36 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1:go_default_library", 37 | "//vendor/github.com/stretchr/testify/assert:go_default_library", 38 | "//vendor/github.com/stretchr/testify/require:go_default_library", 39 | "//vendor/go.uber.org/zap/zaptest:go_default_library", 40 | "//vendor/k8s.io/api/apps/v1:go_default_library", 41 | "//vendor/k8s.io/api/core/v1:go_default_library", 42 | "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", 43 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 44 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 45 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /pkg/specchecker/builtin/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "known_types.go", 7 | "process_deployment.go", 8 | "process_secret.go", 9 | "process_service.go", 10 | "process_service_binding.go", 11 | "process_service_instance.go", 12 | "process_util.go", 13 | ], 14 | importpath = "github.com/atlassian/smith/pkg/specchecker/builtin", 15 | visibility = ["//visibility:public"], 16 | deps = [ 17 | "//:go_default_library", 18 | "//pkg/specchecker:go_default_library", 19 | "//pkg/util:go_default_library", 20 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1:go_default_library", 21 | "//vendor/github.com/pkg/errors:go_default_library", 22 | "//vendor/go.uber.org/zap:go_default_library", 23 | "//vendor/k8s.io/api/apps/v1:go_default_library", 24 | "//vendor/k8s.io/api/core/v1:go_default_library", 25 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 26 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 27 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 28 | "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", 29 | "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", 30 | ], 31 | ) 32 | 33 | go_test( 34 | name = "go_default_test", 35 | size = "small", 36 | srcs = [ 37 | "process_deployment_test.go", 38 | "process_service_instance_test.go", 39 | ], 40 | embed = [":go_default_library"], 41 | race = "on", 42 | deps = [ 43 | "//pkg/specchecker:go_default_library", 44 | "//pkg/specchecker/testing:go_default_library", 45 | "//pkg/util:go_default_library", 46 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1:go_default_library", 47 | "//vendor/github.com/stretchr/testify/assert:go_default_library", 48 | "//vendor/github.com/stretchr/testify/require:go_default_library", 49 | "//vendor/go.uber.org/zap/zaptest:go_default_library", 50 | "//vendor/k8s.io/api/apps/v1:go_default_library", 51 | "//vendor/k8s.io/api/core/v1:go_default_library", 52 | "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", 53 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 54 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 55 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 56 | "//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library", 57 | ], 58 | ) 59 | -------------------------------------------------------------------------------- /pkg/specchecker/builtin/known_types.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/atlassian/smith/pkg/specchecker" 5 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 6 | apps_v1 "k8s.io/api/apps/v1" 7 | core_v1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | ) 10 | 11 | var ( 12 | MainKnownTypes = map[schema.GroupKind]specchecker.ObjectProcessor{ 13 | {Group: apps_v1.GroupName, Kind: "Deployment"}: deployment{}, 14 | {Group: core_v1.GroupName, Kind: "Service"}: service{}, 15 | {Group: core_v1.GroupName, Kind: "Secret"}: secret{}, 16 | } 17 | 18 | ServiceCatalogKnownTypes = map[schema.GroupKind]specchecker.ObjectProcessor{ 19 | {Group: sc_v1b1.GroupName, Kind: "ServiceBinding"}: serviceBinding{}, 20 | {Group: sc_v1b1.GroupName, Kind: "ServiceInstance"}: serviceInstance{}, 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/specchecker/builtin/process_secret.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/atlassian/smith/pkg/specchecker" 5 | "github.com/atlassian/smith/pkg/util" 6 | core_v1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | type secret struct { 12 | } 13 | 14 | func (secret) BeforeCreate(ctx *specchecker.Context, spec *unstructured.Unstructured) (runtime.Object /*updatedSpec*/, error) { 15 | return spec, nil 16 | } 17 | 18 | func (secret) ApplySpec(ctx *specchecker.Context, spec, actual *unstructured.Unstructured) (runtime.Object, error) { 19 | var secretSpec core_v1.Secret 20 | if err := util.ConvertType(coreV1Scheme, spec, &secretSpec); err != nil { 21 | return nil, err 22 | } 23 | 24 | // StringData overwrites Data 25 | if len(secretSpec.StringData) > 0 { 26 | if secretSpec.Data == nil { 27 | secretSpec.Data = make(map[string][]byte, len(secretSpec.StringData)) 28 | } 29 | for k, v := range secretSpec.StringData { 30 | secretSpec.Data[k] = []byte(v) 31 | } 32 | secretSpec.StringData = nil 33 | } 34 | 35 | return &secretSpec, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/specchecker/builtin/process_service.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/atlassian/smith/pkg/specchecker" 5 | "github.com/atlassian/smith/pkg/util" 6 | core_v1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | type service struct { 12 | } 13 | 14 | func (service) BeforeCreate(ctx *specchecker.Context, spec *unstructured.Unstructured) (runtime.Object /*updatedSpec*/, error) { 15 | return spec, nil 16 | } 17 | 18 | func (service) ApplySpec(ctx *specchecker.Context, spec, actual *unstructured.Unstructured) (runtime.Object, error) { 19 | var serviceSpec core_v1.Service 20 | if err := util.ConvertType(coreV1Scheme, spec, &serviceSpec); err != nil { 21 | return nil, err 22 | } 23 | var serviceActual core_v1.Service 24 | if err := util.ConvertType(coreV1Scheme, actual, &serviceActual); err != nil { 25 | return nil, err 26 | } 27 | 28 | serviceSpec.Spec.ClusterIP = serviceActual.Spec.ClusterIP 29 | serviceSpec.Status = serviceActual.Status 30 | 31 | if len(serviceActual.Spec.Ports) == len(serviceSpec.Spec.Ports) { 32 | for i, port := range serviceSpec.Spec.Ports { 33 | if port.NodePort == 0 { 34 | actualPort := serviceActual.Spec.Ports[i] 35 | port.NodePort = actualPort.NodePort 36 | if port == actualPort { // NodePort field is the only difference, other fields are the same 37 | // Copy port from actual if port is not specified in spec 38 | serviceSpec.Spec.Ports[i].NodePort = actualPort.NodePort 39 | } 40 | } 41 | } 42 | } 43 | 44 | return &serviceSpec, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/specchecker/builtin/process_service_binding.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/atlassian/smith/pkg/specchecker" 5 | "github.com/atlassian/smith/pkg/util" 6 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | type serviceBinding struct { 12 | } 13 | 14 | func (serviceBinding) BeforeCreate(ctx *specchecker.Context, spec *unstructured.Unstructured) (runtime.Object /*updatedSpec*/, error) { 15 | return spec, nil 16 | } 17 | 18 | func (serviceBinding) ApplySpec(ctx *specchecker.Context, spec, actual *unstructured.Unstructured) (runtime.Object, error) { 19 | var sbSpec sc_v1b1.ServiceBinding 20 | if err := util.ConvertType(scV1B1Scheme, spec, &sbSpec); err != nil { 21 | return nil, err 22 | } 23 | var sbActual sc_v1b1.ServiceBinding 24 | if err := util.ConvertType(scV1B1Scheme, actual, &sbActual); err != nil { 25 | return nil, err 26 | } 27 | 28 | // managed by service catalog auth filtering just copy to make the comparison work 29 | sbSpec.Spec.UserInfo = sbActual.Spec.UserInfo 30 | 31 | err := setEmptyFieldsFromActual(&sbSpec.Spec, &sbActual.Spec, 32 | // users should never set these ref fields 33 | "InstanceRef", 34 | 35 | // users may set these fields, generally they are autogenerated 36 | "ExternalID", 37 | ) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return &sbSpec, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/specchecker/builtin/process_util.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "reflect" 5 | 6 | sc_v1b1 "github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1" 7 | "github.com/pkg/errors" 8 | apps_v1 "k8s.io/api/apps/v1" 9 | core_v1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | ) 13 | 14 | var ( 15 | appsV1Scheme = runtime.NewScheme() 16 | scV1B1Scheme = runtime.NewScheme() 17 | coreV1Scheme = runtime.NewScheme() 18 | ) 19 | 20 | func init() { 21 | utilruntime.Must(apps_v1.SchemeBuilder.AddToScheme(appsV1Scheme)) 22 | utilruntime.Must(sc_v1b1.SchemeBuilder.AddToScheme(scV1B1Scheme)) 23 | utilruntime.Must(core_v1.SchemeBuilder.AddToScheme(coreV1Scheme)) 24 | } 25 | 26 | // setFieldsFromActual mutates the target with fields from the instantiated object, 27 | // iff that field was not set in the original object. 28 | func setEmptyFieldsFromActual(requested, actual interface{}, fields ...string) error { 29 | requestedValue := reflect.ValueOf(requested).Elem() 30 | actualValue := reflect.ValueOf(actual).Elem() 31 | 32 | if requestedValue.Type() != actualValue.Type() { 33 | return errors.Errorf("attempted to set fields from different types: %q from %q", 34 | requestedValue, actualValue) 35 | } 36 | 37 | for _, field := range fields { 38 | requestedField := requestedValue.FieldByName(field) 39 | if !requestedField.IsValid() { 40 | return errors.Errorf("no such field %q to cleanup", field) 41 | } 42 | actualField := actualValue.FieldByName(field) 43 | if !actualField.IsValid() { 44 | return errors.Errorf("no such field %q to cleanup", field) 45 | } 46 | 47 | if reflect.DeepEqual(requestedField.Interface(), reflect.Zero(requestedField.Type()).Interface()) { 48 | requestedField.Set(actualField) 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/specchecker/testing/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["stuff_for_tests.go"], 6 | importpath = "github.com/atlassian/smith/pkg/specchecker/testing", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//vendor/github.com/pkg/errors:go_default_library", 10 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 11 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/specchecker/testing/stuff_for_tests.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | type FakeStore struct { 10 | Namespace string 11 | Responses map[string]runtime.Object 12 | } 13 | 14 | func (f FakeStore) Get(gvk schema.GroupVersionKind, namespace, name string) (runtime.Object, bool /*exists*/, error) { 15 | if f.Namespace != namespace { 16 | return nil, false, errors.Errorf("namespace does not match: expected %q != passed %q", f.Namespace, namespace) 17 | } 18 | v, ok := f.Responses[name] 19 | return v, ok, nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/specchecker/types.go: -------------------------------------------------------------------------------- 1 | package specchecker 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | type ObjectProcessor interface { 11 | // BeforeCreate pre-processes object specification and returns an updated version. 12 | BeforeCreate(ctx *Context, spec *unstructured.Unstructured) (runtime.Object /*updatedSpec*/, error) 13 | ApplySpec(ctx *Context, spec, actual *unstructured.Unstructured) (runtime.Object /*updatedSpec*/, error) 14 | } 15 | 16 | type Store interface { 17 | Get(gvk schema.GroupVersionKind, namespace, name string) (obj runtime.Object, exists bool, err error) 18 | } 19 | 20 | // Context includes objects used by different cleanup functions 21 | type Context struct { 22 | Logger *zap.Logger 23 | Store Store 24 | } 25 | -------------------------------------------------------------------------------- /pkg/statuschecker/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["checker.go"], 6 | importpath = "github.com/atlassian/smith/pkg/statuschecker", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//:go_default_library", 10 | "//pkg/resources:go_default_library", 11 | "//vendor/github.com/pkg/errors:go_default_library", 12 | "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", 13 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 14 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 15 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/statuschecker/builtin/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["known_types.go"], 6 | importpath = "github.com/atlassian/smith/pkg/statuschecker/builtin", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//pkg/statuschecker:go_default_library", 10 | "//pkg/util:go_default_library", 11 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1:go_default_library", 12 | "//vendor/github.com/pkg/errors:go_default_library", 13 | "//vendor/k8s.io/api/apps/v1:go_default_library", 14 | "//vendor/k8s.io/api/autoscaling/v2beta1:go_default_library", 15 | "//vendor/k8s.io/api/core/v1:go_default_library", 16 | "//vendor/k8s.io/api/networking/v1beta1:go_default_library", 17 | "//vendor/k8s.io/api/policy/v1beta1:go_default_library", 18 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 19 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 20 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 21 | "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", 22 | "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/store/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "bundle.go", 7 | "catalog.go", 8 | "crd.go", 9 | "multi.go", 10 | "multi_basic.go", 11 | ], 12 | importpath = "github.com/atlassian/smith/pkg/store", 13 | visibility = ["//visibility:public"], 14 | deps = [ 15 | "//pkg/apis/smith/v1:go_default_library", 16 | "//pkg/plugin:go_default_library", 17 | "//vendor/github.com/kubernetes-sigs/service-catalog/pkg/apis/servicecatalog/v1beta1:go_default_library", 18 | "//vendor/github.com/pkg/errors:go_default_library", 19 | "//vendor/github.com/xeipuuv/gojsonschema:go_default_library", 20 | "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", 21 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 22 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 23 | "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 24 | "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", 25 | "//vendor/k8s.io/client-go/tools/cache:go_default_library", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /pkg/store/crd.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | apiext_v1b1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "k8s.io/client-go/tools/cache" 8 | ) 9 | 10 | const ( 11 | byGroupKindIndexName = "ByGroupKind" 12 | ) 13 | 14 | type Crd struct { 15 | byIndex func(indexName, indexKey string) ([]interface{}, error) 16 | } 17 | 18 | func NewCrd(crdInf cache.SharedIndexInformer) (*Crd, error) { 19 | err := crdInf.AddIndexers(cache.Indexers{ 20 | byGroupKindIndexName: byGroupKindIndex, 21 | }) 22 | if err != nil { 23 | return nil, errors.WithStack(err) 24 | } 25 | return &Crd{ 26 | byIndex: crdInf.GetIndexer().ByIndex, 27 | }, nil 28 | } 29 | 30 | // Get returns the CRD that defines the resource of provided group and kind. 31 | func (s *Crd) Get(resource schema.GroupKind) (*apiext_v1b1.CustomResourceDefinition, error) { 32 | objs, err := s.byIndex(byGroupKindIndexName, byGroupKindIndexKey(resource.Group, resource.Kind)) 33 | if err != nil { 34 | return nil, err 35 | } 36 | switch len(objs) { 37 | case 0: 38 | return nil, nil 39 | case 1: 40 | crd := objs[0].(*apiext_v1b1.CustomResourceDefinition).DeepCopy() 41 | // Objects from type-specific informers don't have GVK set 42 | crd.Kind = "CustomResourceDefinition" 43 | crd.APIVersion = apiext_v1b1.SchemeGroupVersion.String() 44 | return crd, nil 45 | default: 46 | // Must never happen 47 | panic(errors.Errorf("multiple CRDs by group %q and kind %q: %s", resource.Group, resource.Kind, objs)) 48 | } 49 | } 50 | 51 | func byGroupKindIndex(obj interface{}) ([]string, error) { 52 | crd := obj.(*apiext_v1b1.CustomResourceDefinition) 53 | return []string{byGroupKindIndexKey(crd.Spec.Group, crd.Spec.Names.Kind)}, nil 54 | } 55 | 56 | func byGroupKindIndexKey(group, kind string) string { 57 | return group + "/" + kind 58 | } 59 | -------------------------------------------------------------------------------- /pkg/store/multi.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/apimachinery/pkg/types" 9 | "k8s.io/client-go/tools/cache" 10 | ) 11 | 12 | const ( 13 | ByNamespaceAndControllerUIDIndex = "NamespaceUidIndex" 14 | ) 15 | 16 | type Multi struct { 17 | MultiBasic 18 | } 19 | 20 | func NewMulti() *Multi { 21 | return &Multi{ 22 | MultiBasic: *NewMultiBasic(), 23 | } 24 | } 25 | 26 | // AddInformer adds an Informer to the store. 27 | // Can only be called with a not yet started informer. Otherwise bad things will happen. 28 | func (s *Multi) AddInformer(gvk schema.GroupVersionKind, informer cache.SharedIndexInformer) error { 29 | s.mx.Lock() 30 | defer s.mx.Unlock() 31 | if _, ok := s.informers[gvk]; ok { 32 | return errors.New("informer is already registered") 33 | } 34 | f := informer.GetIndexer().GetIndexers()[ByNamespaceAndControllerUIDIndex] 35 | if f == nil { 36 | // Informer does not have this index yet i.e. this is the first/sole multistore it is added to. 37 | err := informer.AddIndexers(cache.Indexers{ 38 | ByNamespaceAndControllerUIDIndex: byNamespaceAndControllerUIDIndex, 39 | }) 40 | if err != nil { 41 | return errors.WithStack(err) 42 | } 43 | } 44 | s.informers[gvk] = informer 45 | return nil 46 | } 47 | 48 | func (s *Multi) ObjectsControlledBy(namespace string, uid types.UID) ([]runtime.Object, error) { 49 | var result []runtime.Object 50 | indexKey := ByNamespaceAndControllerUIDIndexKey(namespace, uid) 51 | for gvk, inf := range s.GetInformers() { 52 | objs, err := inf.GetIndexer().ByIndex(ByNamespaceAndControllerUIDIndex, indexKey) 53 | if err != nil { 54 | return nil, errors.Wrapf(err, "failed to get objects for bundle from %s informer", gvk) 55 | } 56 | for _, obj := range objs { 57 | ro := obj.(runtime.Object).DeepCopyObject() 58 | ro.GetObjectKind().SetGroupVersionKind(gvk) // Objects from type-specific informers don't have GVK set 59 | result = append(result, ro) 60 | } 61 | } 62 | return result, nil 63 | } 64 | 65 | func byNamespaceAndControllerUIDIndex(obj interface{}) ([]string, error) { 66 | if key, ok := obj.(cache.ExplicitKey); ok { 67 | return []string{string(key)}, nil 68 | } 69 | m := obj.(meta_v1.Object) 70 | ref := meta_v1.GetControllerOf(m) 71 | if ref != nil { 72 | return []string{ByNamespaceAndControllerUIDIndexKey(m.GetNamespace(), ref.UID)}, nil 73 | } 74 | return nil, nil 75 | } 76 | 77 | func ByNamespaceAndControllerUIDIndexKey(namespace string, uid types.UID) string { 78 | if namespace == meta_v1.NamespaceNone { 79 | return string(uid) 80 | } 81 | return namespace + "|" + string(uid) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/store/multi_basic.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/pkg/errors" 7 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/client-go/tools/cache" 11 | ) 12 | 13 | type MultiBasic struct { 14 | mx sync.RWMutex // protects the map 15 | informers map[schema.GroupVersionKind]cache.SharedIndexInformer 16 | } 17 | 18 | func NewMultiBasic() *MultiBasic { 19 | return &MultiBasic{ 20 | informers: make(map[schema.GroupVersionKind]cache.SharedIndexInformer), 21 | } 22 | } 23 | 24 | // AddInformer adds an Informer to the store. 25 | func (s *MultiBasic) AddInformer(gvk schema.GroupVersionKind, informer cache.SharedIndexInformer) error { 26 | s.mx.Lock() 27 | defer s.mx.Unlock() 28 | if _, ok := s.informers[gvk]; ok { 29 | return errors.New("informer is already registered") 30 | } 31 | s.informers[gvk] = informer 32 | return nil 33 | } 34 | 35 | func (s *MultiBasic) RemoveInformer(gvk schema.GroupVersionKind) bool { 36 | s.mx.Lock() 37 | defer s.mx.Unlock() 38 | _, ok := s.informers[gvk] 39 | if ok { 40 | delete(s.informers, gvk) 41 | } 42 | return ok 43 | } 44 | 45 | // GetInformers gets all registered Informers. 46 | func (s *MultiBasic) GetInformers() map[schema.GroupVersionKind]cache.SharedIndexInformer { 47 | s.mx.RLock() 48 | defer s.mx.RUnlock() 49 | informers := make(map[schema.GroupVersionKind]cache.SharedIndexInformer, len(s.informers)) 50 | for gvk, inf := range s.informers { 51 | informers[gvk] = inf 52 | } 53 | return informers 54 | } 55 | 56 | // Get looks up object of specified GVK in the specified namespace by name. 57 | // A deep copy of the object is returned so it is safe to modify it. 58 | func (s *MultiBasic) Get(gvk schema.GroupVersionKind, namespace, name string) (obj runtime.Object, exists bool, e error) { 59 | var informer cache.SharedIndexInformer 60 | func() { 61 | s.mx.RLock() 62 | defer s.mx.RUnlock() 63 | informer = s.informers[gvk] 64 | }() 65 | if informer == nil { 66 | return nil, false, errors.Errorf("no informer for %s is registered", gvk) 67 | } 68 | return s.getFromIndexer(informer.GetIndexer(), gvk, namespace, name) 69 | } 70 | 71 | func (s *MultiBasic) getFromIndexer(indexer cache.Indexer, gvk schema.GroupVersionKind, namespace, name string) (runtime.Object, bool /*exists */, error) { 72 | obj, exists, err := indexer.GetByKey(ByNamespaceAndNameIndexKey(namespace, name)) 73 | if err != nil || !exists { 74 | return nil, exists, err 75 | } 76 | ro := obj.(runtime.Object).DeepCopyObject() 77 | ro.GetObjectKind().SetGroupVersionKind(gvk) // Objects from type-specific informers don't have GVK set 78 | return ro, true, nil 79 | } 80 | 81 | func ByNamespaceAndNameIndexKey(namespace, name string) string { 82 | if namespace == meta_v1.NamespaceNone { 83 | return name 84 | } 85 | return namespace + "/" + name 86 | } 87 | -------------------------------------------------------------------------------- /pkg/util/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["util.go"], 6 | importpath = "github.com/atlassian/smith/pkg/util", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//vendor/github.com/pkg/errors:go_default_library", 10 | "//vendor/k8s.io/api/core/v1:go_default_library", 11 | "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", 12 | "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/util/graph/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "topological_sort.go", 7 | "types.go", 8 | ], 9 | importpath = "github.com/atlassian/smith/pkg/util/graph", 10 | visibility = ["//visibility:public"], 11 | deps = ["//vendor/github.com/pkg/errors:go_default_library"], 12 | ) 13 | 14 | go_test( 15 | name = "go_default_test", 16 | size = "small", 17 | srcs = ["topological_sort_test.go"], 18 | embed = [":go_default_library"], 19 | race = "on", 20 | deps = [ 21 | "//vendor/github.com/stretchr/testify/assert:go_default_library", 22 | "//vendor/github.com/stretchr/testify/require:go_default_library", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/util/graph/topological_sort.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import "github.com/pkg/errors" 4 | 5 | func (g *Graph) TopologicalSort() ([]V, error) { 6 | results := newOrderedSet() 7 | for _, name := range g.orderedVertices { 8 | err := g.visit(name, results, nil) 9 | if err != nil { 10 | return nil, err 11 | } 12 | } 13 | 14 | return results.items, nil 15 | } 16 | 17 | func (g *Graph) visit(name V, results *orderedset, visited *orderedset) error { 18 | if visited == nil { 19 | visited = newOrderedSet() 20 | } 21 | 22 | added := visited.add(name) 23 | if !added { 24 | index := visited.index(name) 25 | cycle := append(visited.items[index:], name) 26 | return errors.Errorf("cycle error: %v", cycle) 27 | } 28 | 29 | n := g.Vertices[name] 30 | for _, edge := range n.OutgoingEdges { 31 | err := g.visit(edge, results, visited.clone()) 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | 37 | results.add(name) 38 | return nil 39 | } 40 | 41 | type orderedset struct { 42 | indexes map[V]int 43 | items []V 44 | length int 45 | } 46 | 47 | func newOrderedSet() *orderedset { 48 | return &orderedset{ 49 | indexes: make(map[V]int), 50 | length: 0, 51 | } 52 | } 53 | 54 | func (s *orderedset) add(item V) bool { 55 | _, ok := s.indexes[item] 56 | if ok { 57 | return false 58 | } 59 | s.indexes[item] = s.length 60 | s.items = append(s.items, item) 61 | s.length++ 62 | return true 63 | } 64 | 65 | func (s *orderedset) clone() *orderedset { 66 | clone := newOrderedSet() 67 | for _, item := range s.items { 68 | clone.add(item) 69 | } 70 | return clone 71 | } 72 | 73 | func (s *orderedset) index(item V) int { 74 | index, ok := s.indexes[item] 75 | if ok { 76 | return index 77 | } 78 | return -1 79 | } 80 | -------------------------------------------------------------------------------- /pkg/util/graph/types.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import "github.com/pkg/errors" 4 | 5 | // V is name of the vertex. 6 | type V interface{} 7 | 8 | // D is data attached to the vertex. 9 | type D interface{} 10 | 11 | // Vertex is a resource representation in a dependency graph. 12 | type Vertex struct { 13 | // Edges in order of appearance (for deterministic order after sort). 14 | OutgoingEdges []V 15 | // Edges in the inverse of outgoing, i.e. vertices that depend on this vertex 16 | IncomingEdges []V 17 | Data D 18 | } 19 | 20 | // Graph is a graph representation of resource dependencies. 21 | type Graph struct { 22 | // Vertices is a map from resource name to resource vertex. 23 | Vertices map[V]*Vertex 24 | 25 | // Vertices in order of appearance (for deterministic order after sort). 26 | orderedVertices []V 27 | } 28 | 29 | func NewGraph(size int) *Graph { 30 | return &Graph{ 31 | Vertices: make(map[V]*Vertex, size), 32 | orderedVertices: make([]V, 0, size), 33 | } 34 | } 35 | 36 | func (g *Graph) AddVertex(name V, data D) { 37 | if !g.ContainsVertex(name) { 38 | g.Vertices[name] = &Vertex{ 39 | Data: data, 40 | } 41 | g.orderedVertices = append(g.orderedVertices, name) 42 | } 43 | } 44 | 45 | func (g *Graph) AddEdge(from, to V) error { 46 | f, ok := g.Vertices[from] 47 | if !ok { 48 | return errors.Errorf("vertex %q not found", from) 49 | } 50 | t, ok := g.Vertices[to] 51 | if !ok { 52 | return errors.Errorf("vertex %q not found", to) 53 | } 54 | 55 | f.addOutgoingEdge(to) 56 | t.addIncomingEdge(from) 57 | return nil 58 | } 59 | 60 | func (g *Graph) ContainsVertex(name V) bool { 61 | _, ok := g.Vertices[name] 62 | return ok 63 | } 64 | 65 | func (v *Vertex) addOutgoingEdge(name V) { 66 | v.OutgoingEdges = append(v.OutgoingEdges, name) 67 | } 68 | 69 | func (v *Vertex) addIncomingEdge(name V) { 70 | v.IncomingEdges = append(v.IncomingEdges, name) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/util/logz/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["logz.go"], 6 | importpath = "github.com/atlassian/smith/pkg/util/logz", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//pkg/apis/smith/v1:go_default_library", 10 | "//vendor/go.uber.org/zap:go_default_library", 11 | "//vendor/go.uber.org/zap/zapcore:go_default_library", 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/util/logz/logz.go: -------------------------------------------------------------------------------- 1 | package logz 2 | 3 | import ( 4 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zapcore" 7 | ) 8 | 9 | func Resource(resourceName smith_v1.ResourceName) zapcore.Field { 10 | return zap.String("resource", string(resourceName)) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/util/testing/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["utils_for_tests.go"], 6 | importpath = "github.com/atlassian/smith/pkg/util/testing", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//pkg/apis/smith/v1:go_default_library", 10 | "//vendor/github.com/atlassian/ctrl/apis/condition/v1:go_default_library", 11 | "//vendor/github.com/stretchr/testify/assert:go_default_library", 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/util/testing/utils_for_tests.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "testing" 5 | 6 | cond_v1 "github.com/atlassian/ctrl/apis/condition/v1" 7 | smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func AssertCondition(t *testing.T, bundle *smith_v1.Bundle, conditionType cond_v1.ConditionType, status cond_v1.ConditionStatus) *cond_v1.Condition { 12 | _, condition := cond_v1.FindCondition(bundle.Status.Conditions, conditionType) 13 | if assert.NotNil(t, condition) { 14 | assert.Equal(t, status, condition.Status, "%s: %s: %s", conditionType, condition.Reason, condition.Message) 15 | } 16 | return condition 17 | } 18 | 19 | func AssertResourceCondition(t *testing.T, bundle *smith_v1.Bundle, resName smith_v1.ResourceName, conditionType cond_v1.ConditionType, status cond_v1.ConditionStatus) *cond_v1.Condition { 20 | _, resStatus := bundle.Status.GetResourceStatus(resName) 21 | if !assert.NotNil(t, resStatus, "%s", resName) { 22 | return nil 23 | } 24 | _, condition := cond_v1.FindCondition(resStatus.Conditions, conditionType) 25 | if !assert.NotNil(t, condition, "%s: %s", resName, conditionType) { 26 | return nil 27 | } 28 | assert.Equal(t, status, condition.Status, "%s: %s: %s: %s", resName, conditionType, condition.Reason, condition.Message) 29 | return condition 30 | } 31 | 32 | func AssertResourceConditionMessage(t *testing.T, bundle *smith_v1.Bundle, resName smith_v1.ResourceName, conditionType cond_v1.ConditionType, message string) *cond_v1.Condition { 33 | _, resStatus := bundle.Status.GetResourceStatus(resName) 34 | if !assert.NotNil(t, resStatus, "%s", resName) { 35 | return nil 36 | } 37 | _, condition := cond_v1.FindCondition(resStatus.Conditions, conditionType) 38 | if !assert.NotNil(t, condition, "%s: %s", resName, conditionType) { 39 | return nil 40 | } 41 | assert.Equal(t, message, condition.Message, "%s: %s: %s: %s", resName, conditionType, condition.Reason, condition.Message) 42 | return condition 43 | } 44 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | core_v1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | func Sleep(ctx context.Context, d time.Duration) error { 14 | timer := time.NewTimer(d) 15 | select { 16 | case <-ctx.Done(): 17 | timer.Stop() 18 | return ctx.Err() 19 | case <-timer.C: 20 | return nil 21 | } 22 | } 23 | 24 | // ConvertType should be used to convert to typed objects. 25 | // If the in object is unstructured then it must have GVK set otherwise it must be 26 | // recognizable by scheme or have the GVK set. 27 | func ConvertType(scheme *runtime.Scheme, in, out runtime.Object) error { 28 | in = in.DeepCopyObject() 29 | if err := scheme.Convert(in, out, nil); err != nil { 30 | return err 31 | } 32 | gvkOut := out.GetObjectKind().GroupVersionKind() 33 | if gvkOut.Kind == "" || gvkOut.Version == "" { // Group can be empty 34 | // API machinery discards TypeMeta for typed objects. This is annoying. 35 | gvks, _, err := scheme.ObjectKinds(in) 36 | if err != nil { 37 | return err 38 | } 39 | out.GetObjectKind().SetGroupVersionKind(gvks[0]) 40 | } 41 | return nil 42 | } 43 | 44 | // RuntimeToUnstructured can be used to convert any typed or unstructured object into 45 | // an unstructured object. The obj must have GVK set. 46 | func RuntimeToUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) { 47 | gvk := obj.GetObjectKind().GroupVersionKind() 48 | if gvk.Kind == "" || gvk.Version == "" { // Group can be empty 49 | return nil, errors.Errorf("cannot convert %T to Unstructured: object Kind and/or object Version is empty", obj) 50 | } 51 | u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj.DeepCopyObject()) 52 | if err != nil { 53 | return nil, errors.WithStack(err) 54 | } 55 | return &unstructured.Unstructured{ 56 | Object: u, 57 | }, nil 58 | } 59 | 60 | func IsSecret(obj runtime.Object) bool { 61 | gvk := obj.GetObjectKind().GroupVersionKind() 62 | return gvk.Group == core_v1.GroupName && gvk.Kind == "Secret" 63 | } 64 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package smith 4 | 5 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 6 | 7 | import ( 8 | _ "k8s.io/code-generator/cmd/client-gen" 9 | _ "k8s.io/code-generator/cmd/deepcopy-gen" 10 | ) 11 | --------------------------------------------------------------------------------