diff --git a/.travis.yml b/.travis.yml index 461a8fb65f3..5c0476bd725 100644 --- a/.travis.yml +++ b/.travis.yml @@ -103,7 +103,9 @@ jobs: # Build and test go - <<: *test name: Go on Kubernetes - script: make test-e2e-go + script: + - make test-e2e-go + - make test-integration # Build and test ansible and test ansible using molecule - <<: *test diff --git a/Makefile b/Makefile index b11865ef329..8b0fbb8b073 100644 --- a/Makefile +++ b/Makefile @@ -185,7 +185,7 @@ test-sanity test/sanity: tidy build/operator-sdk ./hack/tests/sanity-check.sh ./hack/tests/check-lint.sh ci -TEST_PKGS:=$(shell go list ./... | grep -v -P 'github.com/operator-framework/operator-sdk/(hack|test/e2e)') +TEST_PKGS:=$(shell go list ./... | grep -v -P 'github.com/operator-framework/operator-sdk/(hack/|test/)') test-unit test/unit: ## Run the unit tests $(Q)go test -coverprofile=coverage.out -covermode=count -count=1 -short ${TEST_PKGS} @@ -208,8 +208,8 @@ test-subcommand-scorecard: test-subcommand-olm-install: ./hack/tests/subcommand-olm-install.sh -# E2E tests. -.PHONY: test-e2e test-e2e-go test-e2e-ansible test-e2e-ansible-molecule test-e2e-helm +# E2E and integration tests. +.PHONY: test-e2e test-e2e-go test-e2e-ansible test-e2e-ansible-molecule test-e2e-helm test-integration test-e2e: test-e2e-go test-e2e-ansible test-e2e-ansible-molecule test-e2e-helm ## Run the e2e tests @@ -224,3 +224,6 @@ test-e2e-ansible-molecule: image-build-ansible test-e2e-helm: image-build-helm ./hack/tests/e2e-helm.sh + +test-integration: + ./hack/tests/integration.sh diff --git a/ci/prow.Makefile b/ci/prow.Makefile index 8e5cd634ea8..1e202a8b4f5 100644 --- a/ci/prow.Makefile +++ b/ci/prow.Makefile @@ -16,3 +16,6 @@ test-e2e-helm test/e2e/helm: test-subcommand test/subcommand: ./ci/tests/subcommand.sh + +test-integration: + ./ci/tests/integration.sh diff --git a/ci/tests/integration.sh b/ci/tests/integration.sh new file mode 100644 index 00000000000..c1412e5f07f --- /dev/null +++ b/ci/tests/integration.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -eux + +# Prow CI setup. +component="memcached-operator" +eval IMAGE=$IMAGE_FORMAT +export OSDK_INTEGRATION_IMAGE="$IMAGE" + +# Integration tests will use default loading rules for the kubeconfig if +# KUBECONFIG is not set. +# Assumes OLM is installed. +go test -v ./test/integration diff --git a/cmd/operator-sdk/alpha/cmd.go b/cmd/operator-sdk/alpha/cmd.go index 31e56acaf1f..de389a4f042 100644 --- a/cmd/operator-sdk/alpha/cmd.go +++ b/cmd/operator-sdk/alpha/cmd.go @@ -16,6 +16,7 @@ package alpha import ( "github.com/operator-framework/operator-sdk/cmd/operator-sdk/alpha/olm" + "github.com/spf13/cobra" ) diff --git a/cmd/operator-sdk/alpha/olm/cmd.go b/cmd/operator-sdk/alpha/olm/cmd.go index 13fb3c00f1f..03242337ade 100644 --- a/cmd/operator-sdk/alpha/olm/cmd.go +++ b/cmd/operator-sdk/alpha/olm/cmd.go @@ -21,12 +21,14 @@ import ( func NewCmd() *cobra.Command { cmd := &cobra.Command{ Use: "olm", - Short: "Manage the Operator Lifecycle Manager installation in your cluster", + Short: "Manage the Operator Lifecycle Manager installation and Operators in your cluster", } cmd.AddCommand( NewInstallCmd(), NewUninstallCmd(), NewStatusCmd(), + NewUpCmd(), + NewDownCmd(), ) return cmd } diff --git a/cmd/operator-sdk/alpha/olm/down.go b/cmd/operator-sdk/alpha/olm/down.go new file mode 100644 index 00000000000..364fead6806 --- /dev/null +++ b/cmd/operator-sdk/alpha/olm/down.go @@ -0,0 +1,44 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "fmt" + "log" + + olmoperator "github.com/operator-framework/operator-sdk/internal/olm/operator" + + "github.com/spf13/cobra" +) + +func NewDownCmd() *cobra.Command { + c := &olmoperator.OLMCmd{} + cmd := &cobra.Command{ + Use: "down", + Short: "Tears down the operator with Operator Lifecycle Manager", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("command %q requires exactly one argument", cmd.CommandPath()) + } + c.ManifestsDir = args[0] + if err := c.Down(); err != nil { + log.Fatalf("Failed to uninstall operator: %v", err) + } + return nil + }, + } + c.AddToFlagSet(cmd.Flags()) + return cmd +} diff --git a/cmd/operator-sdk/alpha/olm/up.go b/cmd/operator-sdk/alpha/olm/up.go new file mode 100644 index 00000000000..4ea43de5112 --- /dev/null +++ b/cmd/operator-sdk/alpha/olm/up.go @@ -0,0 +1,44 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "fmt" + "log" + + olmoperator "github.com/operator-framework/operator-sdk/internal/olm/operator" + + "github.com/spf13/cobra" +) + +func NewUpCmd() *cobra.Command { + c := &olmoperator.OLMCmd{} + cmd := &cobra.Command{ + Use: "up", + Short: "Launches the operator with Operator Lifecycle Manager", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("command %q requires exactly one argument", cmd.CommandPath()) + } + c.ManifestsDir = args[0] + if err := c.Up(); err != nil { + log.Fatalf("Failed to install operator: %v", err) + } + return nil + }, + } + c.AddToFlagSet(cmd.Flags()) + return cmd +} diff --git a/doc/cli/operator-sdk_alpha.md b/doc/cli/operator-sdk_alpha.md index cf209d2b328..dd06e3ee093 100644 --- a/doc/cli/operator-sdk_alpha.md +++ b/doc/cli/operator-sdk_alpha.md @@ -15,5 +15,5 @@ Run an alpha subcommand ### SEE ALSO * [operator-sdk](operator-sdk.md) - An SDK for building operators with ease -* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation in your cluster +* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation and Operators in your cluster diff --git a/doc/cli/operator-sdk_alpha_olm.md b/doc/cli/operator-sdk_alpha_olm.md index f65d097ae95..ae7e6cbe1f7 100644 --- a/doc/cli/operator-sdk_alpha_olm.md +++ b/doc/cli/operator-sdk_alpha_olm.md @@ -1,10 +1,10 @@ ## operator-sdk alpha olm -Manage the Operator Lifecycle Manager installation in your cluster +Manage the Operator Lifecycle Manager installation and Operators in your cluster ### Synopsis -Manage the Operator Lifecycle Manager installation in your cluster +Manage the Operator Lifecycle Manager installation and Operators in your cluster ### Options @@ -15,7 +15,9 @@ Manage the Operator Lifecycle Manager installation in your cluster ### SEE ALSO * [operator-sdk alpha](operator-sdk_alpha.md) - Run an alpha subcommand +* [operator-sdk alpha olm down](operator-sdk_alpha_olm_down.md) - Tears down the operator with Operator Lifecycle Manager * [operator-sdk alpha olm install](operator-sdk_alpha_olm_install.md) - Install Operator Lifecycle Manager in your cluster * [operator-sdk alpha olm status](operator-sdk_alpha_olm_status.md) - Get the status of the Operator Lifecycle Manager installation in your cluster * [operator-sdk alpha olm uninstall](operator-sdk_alpha_olm_uninstall.md) - Uninstall Operator Lifecycle Manager from your cluster +* [operator-sdk alpha olm up](operator-sdk_alpha_olm_up.md) - Launches the operator with Operator Lifecycle Manager diff --git a/doc/cli/operator-sdk_alpha_olm_down.md b/doc/cli/operator-sdk_alpha_olm_down.md new file mode 100644 index 00000000000..8cfb8e5405f --- /dev/null +++ b/doc/cli/operator-sdk_alpha_olm_down.md @@ -0,0 +1,29 @@ +## operator-sdk alpha olm down + +Tears down the operator with Operator Lifecycle Manager + +### Synopsis + +Tears down the operator with Operator Lifecycle Manager + +``` +operator-sdk alpha olm down [flags] +``` + +### Options + +``` + --force Force operator-sdk up/down to overwrite/delete all resources known to the command, respectively + -h, --help help for down + --include strings Path to Kubernetes resource manifests, ex. Role, Subscription. These supplement or override defaults generated by up/down + --install-mode string InstallMode to create OperatorGroup with. Format: InstallModeType=[ns1,ns2[, ...]] + --kubeconfig string Path to kubeconfig + --namespace string Namespace in which to create resources + --operator-version string Version of operator to deploy + --timeout duration Time to wait for the command to complete before failing (default 2m0s) +``` + +### SEE ALSO + +* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation and Operators in your cluster + diff --git a/doc/cli/operator-sdk_alpha_olm_install.md b/doc/cli/operator-sdk_alpha_olm_install.md index aa62474d40f..a4cc724c7f6 100644 --- a/doc/cli/operator-sdk_alpha_olm_install.md +++ b/doc/cli/operator-sdk_alpha_olm_install.md @@ -20,5 +20,5 @@ operator-sdk alpha olm install [flags] ### SEE ALSO -* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation in your cluster +* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation and Operators in your cluster diff --git a/doc/cli/operator-sdk_alpha_olm_status.md b/doc/cli/operator-sdk_alpha_olm_status.md index 52bc68703b7..adba608b346 100644 --- a/doc/cli/operator-sdk_alpha_olm_status.md +++ b/doc/cli/operator-sdk_alpha_olm_status.md @@ -19,5 +19,5 @@ operator-sdk alpha olm status [flags] ### SEE ALSO -* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation in your cluster +* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation and Operators in your cluster diff --git a/doc/cli/operator-sdk_alpha_olm_uninstall.md b/doc/cli/operator-sdk_alpha_olm_uninstall.md index b74539dd087..31d32d271c0 100644 --- a/doc/cli/operator-sdk_alpha_olm_uninstall.md +++ b/doc/cli/operator-sdk_alpha_olm_uninstall.md @@ -19,5 +19,5 @@ operator-sdk alpha olm uninstall [flags] ### SEE ALSO -* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation in your cluster +* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation and Operators in your cluster diff --git a/doc/cli/operator-sdk_alpha_olm_up.md b/doc/cli/operator-sdk_alpha_olm_up.md new file mode 100644 index 00000000000..9f6b7929b6a --- /dev/null +++ b/doc/cli/operator-sdk_alpha_olm_up.md @@ -0,0 +1,29 @@ +## operator-sdk alpha olm up + +Launches the operator with Operator Lifecycle Manager + +### Synopsis + +Launches the operator with Operator Lifecycle Manager + +``` +operator-sdk alpha olm up [flags] +``` + +### Options + +``` + --force Force operator-sdk up/down to overwrite/delete all resources known to the command, respectively + -h, --help help for up + --include strings Path to Kubernetes resource manifests, ex. Role, Subscription. These supplement or override defaults generated by up/down + --install-mode string InstallMode to create OperatorGroup with. Format: InstallModeType=[ns1,ns2[, ...]] + --kubeconfig string Path to kubeconfig + --namespace string Namespace in which to create resources + --operator-version string Version of operator to deploy + --timeout duration Time to wait for the command to complete before failing (default 2m0s) +``` + +### SEE ALSO + +* [operator-sdk alpha olm](operator-sdk_alpha_olm.md) - Manage the Operator Lifecycle Manager installation and Operators in your cluster + diff --git a/go.mod b/go.mod index 37ca8152b03..623d0588bad 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,16 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/coreos/go-semver v0.3.0 github.com/coreos/prometheus-operator v0.34.0 + github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect + github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect + github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/fatih/structtag v1.1.0 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-logr/logr v0.1.0 github.com/go-logr/zapr v0.1.1 github.com/gobuffalo/packr v1.30.1 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gregjones/httpcache v0.0.0-20190203031600-7a902570cb17 // indirect + github.com/gorilla/websocket v1.4.1 // indirect github.com/huandu/xstrings v1.2.0 // indirect github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 github.com/jmoiron/sqlx v1.2.0 // indirect @@ -28,8 +31,8 @@ require ( github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.1.2 - github.com/operator-framework/operator-lifecycle-manager v0.0.0-20191115003340-16619cd27fa5 - github.com/operator-framework/operator-registry v1.5.1 + github.com/operator-framework/operator-lifecycle-manager v0.0.0-20191115003340-16619cd27fa5 // 0.13.0 + github.com/operator-framework/operator-registry v1.5.3 github.com/pborman/uuid v1.2.0 github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v1.1.0 diff --git a/go.sum b/go.sum index 6058ba532b0..a8d5bc4f45b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690/go.mod h1:Ulb78X89vxKYgdL24HMTiXYHlyHEvruOj1ZPlqeNEZM= +bou.ke/monkey v1.0.1 h1:zEMLInw9xvNakzUUPjfS4Ds6jYPqCFx3m7bRmG5NH2U= bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -59,6 +60,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/ant31/crd-validation v0.0.0-20180702145049-30f8a35d0ac2/go.mod h1:X0noFIik9YqfhGYBLEHg8LJKEwy7QIitLQuFMpKLcPk= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6 h1:uZuxRZCz65cG1o6K/xUqImNcYKtmk9ylqaH0itMSvzA= github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -165,6 +167,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/libnetwork v0.0.0-20180830151422-a9cd636e3789/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -173,6 +177,10 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f h1:8GDPb0tCY8LQ+OJ3dbHb5sA6YZWXFORQYZx5sdsTlMs= +github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f h1:AUj1VoZUfhPhOPHULCQQDnGhRelpFWHMLhQVWDsS0v4= +github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.6+incompatible h1:tfrHha8zJ01ywiOEC1miGY8st1/igzWB8OmvPgoYX7w= @@ -280,6 +288,7 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09Vjb github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48 h1:X+zN6RZXsvnrSJaAIQhZezPfAfvsqihKKR8oiLHid34= github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-migrate/migrate/v4 v4.6.2 h1:LDDOHo/q1W5UDj6PbkxdCv7lv9yunyZHXvxuwDkGo3k= github.com/golang-migrate/migrate/v4 v4.6.2/go.mod h1:JYi6reN3+Z734VZ0akNuyOJNcrg45ZL7LDBMW3WGJL0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -340,9 +349,10 @@ github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20190203031600-7a902570cb17 h1:prg2TTpTOcJF1jRWL2zSU1FQNgB0STAFNux8GK82y8k= -github.com/gregjones/httpcache v0.0.0-20190203031600-7a902570cb17/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -525,12 +535,16 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/operator-framework/operator-lifecycle-manager v0.0.0-20191115003340-16619cd27fa5 h1:rjaihxY50c5C+kbQIK4s36R8zxByATYrgRbua4eiG6o= github.com/operator-framework/operator-lifecycle-manager v0.0.0-20191115003340-16619cd27fa5/go.mod h1:zL34MNy92LPutBH5gQK+gGhtgTUlZZX03I2G12vWHF4= -github.com/operator-framework/operator-registry v1.5.1 h1:8ruUOG6IBDVTAXYWKsv6hwr4yv/0SFPFPAYGCpcv97E= github.com/operator-framework/operator-registry v1.5.1/go.mod h1:agrQlkWOo1q8U1SAaLSS2WQ+Z9vswNT2M2HFib9iuLY= +github.com/operator-framework/operator-registry v1.5.3 h1:az83WDwgB+tHsmVn+tFq72yQBbaUAye8e4+KkDQmzLs= +github.com/operator-framework/operator-registry v1.5.3/go.mod h1:agrQlkWOo1q8U1SAaLSS2WQ+Z9vswNT2M2HFib9iuLY= +github.com/otiai10/copy v1.0.1 h1:gtBjD8aq4nychvRZ2CyJvFWAw0aja+VHazDdruZKGZA= github.com/otiai10/copy v1.0.1/go.mod h1:8bMCJrAqOtN/d9oyh5HR7HhLQMvcGMpGdwRDYsfOCHc= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 h1:o59bHXu8Ejas8Kq6pjoVJQ9/neN66SM8AKh6wI42BBs= github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4= github.com/otiai10/mint v1.2.3/go.mod h1:YnfyPNhBvnY8bW4SGQHCs/aAFhkgySlMZbrF5U0bOVw= +github.com/otiai10/mint v1.2.4 h1:DxYL0itZyPaR5Z9HILdxSoHx+gNs6Yx+neOGS3IVUk0= github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -588,6 +602,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uY github.com/robfig/cron v0.0.0-20170526150127-736158dc09e1/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw= diff --git a/hack/tests/integration.sh b/hack/tests/integration.sh new file mode 100755 index 00000000000..abd2584b76f --- /dev/null +++ b/hack/tests/integration.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -eu + +source hack/lib/image_lib.sh + +export OSDK_INTEGRATION_IMAGE="quay.io/example/memcached-operator:v0.0.1" + +# Build the operator image. +pushd test/test-framework +operator-sdk build "$OSDK_INTEGRATION_IMAGE" +# If using a kind cluster, load the image into all nodes. +load_image_if_kind "$OSDK_INTEGRATION_IMAGE" +popd + +# Install OLM on the cluster if not installed. +olm_latest_exists=0 +if ! operator-sdk alpha olm status > /dev/null 2>&1; then + operator-sdk alpha olm install + olm_latest_exists=1 +fi + +# Integration tests will use default loading rules for the kubeconfig if +# KUBECONFIG is not set. +go test -v ./test/integration + +# Uninstall OLM if it was installed for test purposes. +if eval "(( $olm_latest_exists ))"; then + operator-sdk alpha olm uninstall +fi + +echo -e "\n=== Integration tests succeeded ===\n" diff --git a/internal/olm/operator/internal/configmap.go b/internal/olm/operator/internal/configmap.go new file mode 100644 index 00000000000..dedd304c213 --- /dev/null +++ b/internal/olm/operator/internal/configmap.go @@ -0,0 +1,152 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "context" + "crypto/md5" + "encoding/base32" + "fmt" + "strings" + + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" + + "github.com/ghodss/yaml" + "github.com/operator-framework/operator-registry/pkg/registry" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +const ( + // The directory containing all manifests for an operator, with the + // package manifest being top-level. + containerManifestsDir = "/registry/manifests" +) + +// IsManifestDataStale checks if manifest data stored in the registry is stale +// by comparing it to manifest data currently managed by m. +func (m *RegistryResources) IsManifestDataStale(ctx context.Context, namespace string) (bool, error) { + pkg := m.Manifests.GetPackageManifest() + pkgName := pkg.PackageName + nn := types.NamespacedName{ + Name: getRegistryConfigMapName(pkgName), + Namespace: namespace, + } + configmap := corev1.ConfigMap{} + err := m.Client.KubeClient.Get(ctx, nn, &configmap) + if err != nil { + return false, err + } + // Collect digests of manifests submitted to m. + newData, err := createConfigMapBinaryData(pkg, m.Manifests.GetBundles()) + if err != nil { + return false, errors.Wrap(err, "error creating binary data") + } + // If the number of files to be added to the registry don't match the number + // of files currently in the registry, we have added or removed a file. + if len(newData) != len(configmap.BinaryData) { + return true, nil + } + // Check each binary value's key, which contains a base32-encoded md5 digest + // component, against the new set of manifest keys. + for fileKey := range configmap.BinaryData { + if _, match := newData[fileKey]; !match { + return true, nil + } + } + return false, nil +} + +// hashContents creates a base32-encoded md5 digest of b's bytes. +func hashContents(b []byte) string { + h := md5.New() + _, _ = h.Write(b) + enc := base32.StdEncoding.WithPadding(base32.NoPadding) + return enc.EncodeToString(h.Sum(nil)) +} + +// getObjectFileName opaquely creates a unique file name based on data in b. +func getObjectFileName(b []byte, name, kind string) string { + digest := hashContents(b) + return fmt.Sprintf("%s.%s.%s.yaml", digest, name, strings.ToLower(kind)) +} + +func getPackageFileName(b []byte, name string) string { + digest := hashContents(b) + return fmt.Sprintf("%s.%s.package.yaml", digest, name) +} + +// createConfigMapBinaryData opaquely creates a set of paths using data in pkg +// and each bundle in bundles, unique by path. These paths are intended to +// be keys in a ConfigMap. +func createConfigMapBinaryData(pkg registry.PackageManifest, bundles []*registry.Bundle) (map[string][]byte, error) { + pkgName := pkg.PackageName + binaryKeyValues := map[string][]byte{} + pb, err := yaml.Marshal(pkg) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling package manifest %s", pkgName) + } + binaryKeyValues[getPackageFileName(pb, pkgName)] = pb + for _, bundle := range bundles { + for _, o := range bundle.Objects { + ob, err := yaml.Marshal(o) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling object %s %q", o.GroupVersionKind(), o.GetName()) + } + binaryKeyValues[getObjectFileName(ob, o.GetName(), o.GetKind())] = ob + } + } + return binaryKeyValues, nil +} + +func getRegistryConfigMapName(pkgName string) string { + name := k8sutil.FormatOperatorNameDNS1123(pkgName) + return fmt.Sprintf("%s-registry-bundles", name) +} + +// withBinaryData returns a function that creates entries in the ConfigMap +// argument's binaryData for each key and []byte value in kvs. +func withBinaryData(kvs map[string][]byte) func(*corev1.ConfigMap) { + return func(cm *corev1.ConfigMap) { + if cm.BinaryData == nil { + cm.BinaryData = map[string][]byte{} + } + for k, v := range kvs { + cm.BinaryData[k] = v + } + } +} + +// newRegistryConfigMap creates a new ConfigMap with a name derived from +// pkgName, the package manifest's packageName, in namespace. opts will +// be applied to the ConfigMap object. +func newRegistryConfigMap(pkgName, namespace string, opts ...func(*corev1.ConfigMap)) *corev1.ConfigMap { + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getRegistryConfigMapName(pkgName), + Namespace: namespace, + }, + } + for _, opt := range opts { + opt(cm) + } + return cm +} diff --git a/internal/olm/operator/internal/deployment.go b/internal/olm/operator/internal/deployment.go new file mode 100644 index 00000000000..639e2ae0ed0 --- /dev/null +++ b/internal/olm/operator/internal/deployment.go @@ -0,0 +1,176 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "fmt" + + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // The image operator-registry's initializer and registry-server binaries + // are run from. + // QUESTION(estroz): version registry image? + registryBaseImage = "quay.io/openshift/origin-operator-registry:latest" + // The port registry-server will listen on within a container. + registryGRPCPort = 50051 + // Path of the bundle database generated by initializer. + regisryDBName = "bundle.db" + // Path of the log file generated by registry-server. + // TODO(estroz): have this log file in an obvious place, ex. /var/log. + registryLogFile = "termination.log" +) + +func getRegistryServerName(pkgName string) string { + name := k8sutil.FormatOperatorNameDNS1123(pkgName) + return fmt.Sprintf("%s-registry-server", name) +} + +func getRegistryVolumeName(pkgName string) string { + name := k8sutil.FormatOperatorNameDNS1123(pkgName) + return fmt.Sprintf("%s-bundle-db", name) +} + +// getRegistryDeploymentLabels creates a set of labels to identify +// operator-registry Deployment objects. +func getRegistryDeploymentLabels(pkgName string) map[string]string { + labels := map[string]string{ + "name": getRegistryServerName(pkgName), + } + for k, v := range SDKLabels { + labels[k] = v + } + return labels +} + +// applyToDeploymentPodSpec applies f to dep's pod template spec. +func applyToDeploymentPodSpec(dep *appsv1.Deployment, f func(*corev1.PodSpec)) { + f(&dep.Spec.Template.Spec) +} + +// withVolumeConfigMap returns a function that appends a volume with name +// volName containing a reference to a ConfigMap with name cmName to the +// Deployment argument's pod template spec. +func withVolumeConfigMap(volName, cmName string) func(*appsv1.Deployment) { + volume := corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cmName, + }, + }, + }, + } + return func(dep *appsv1.Deployment) { + applyToDeploymentPodSpec(dep, func(spec *corev1.PodSpec) { + spec.Volumes = append(spec.Volumes, volume) + }) + } +} + +// withContainerVolumeMounts returns a function that appends volumeMounts +// to each container in the Deployment argument's pod template spec. One +// volumeMount is appended for each path in paths from volume with name +// volName. +func withContainerVolumeMounts(volName string, paths []string) func(*appsv1.Deployment) { + volumeMounts := []corev1.VolumeMount{} + for _, p := range paths { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: p, + }) + } + return func(dep *appsv1.Deployment) { + applyToDeploymentPodSpec(dep, func(spec *corev1.PodSpec) { + for i := range spec.Containers { + spec.Containers[i].VolumeMounts = append(spec.Containers[i].VolumeMounts, volumeMounts...) + } + }) + } +} + +// getDBContainerCmd returns a command string that, when run, does two things: +// 1. Runs a database initializer on the manifests in the current working +// directory. +// 2. Runs an operator-registry server serving the bundle database. +// The database must be in the current working directory. +func getDBContainerCmd(dbPath, logPath string) string { + initCmd := fmt.Sprintf("/usr/bin/initializer -o %s", dbPath) + srvCmd := fmt.Sprintf("/usr/bin/registry-server -d %s -t %s", dbPath, logPath) + return fmt.Sprintf("%s && %s", initCmd, srvCmd) +} + +// withRegistryGRPCContainer returns a function that appends a container +// running an operator-registry GRPC server to the Deployment argument's +// pod template spec. +func withRegistryGRPCContainer(pkgName string) func(*appsv1.Deployment) { + container := corev1.Container{ + Name: getRegistryServerName(pkgName), + Image: registryBaseImage, + Command: []string{"/bin/bash"}, + Args: []string{ + "-c", + // TODO(estroz): grab logs and print if error + getDBContainerCmd(regisryDBName, registryLogFile), + }, + Ports: []corev1.ContainerPort{ + {Name: "registry-grpc", ContainerPort: registryGRPCPort}, + }, + } + return func(dep *appsv1.Deployment) { + applyToDeploymentPodSpec(dep, func(spec *corev1.PodSpec) { + spec.Containers = append(spec.Containers, container) + }) + } +} + +// newRegistryDeployment creates a new Deployment with a name derived from +// pkgName, the package manifest's packageName, in namespace. The Deployment +// and replicas are created with labels derived from pkgName. opts will be +// applied to the Deployment object. +func newRegistryDeployment(pkgName, namespace string, opts ...func(*appsv1.Deployment)) *appsv1.Deployment { + var replicas int32 = 1 + dep := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getRegistryServerName(pkgName), + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: getRegistryDeploymentLabels(pkgName), + }, + Replicas: &replicas, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: getRegistryDeploymentLabels(pkgName), + }, + }, + }, + } + for _, opt := range opts { + opt(dep) + } + return dep +} diff --git a/internal/olm/operator/internal/registry.go b/internal/olm/operator/internal/registry.go new file mode 100644 index 00000000000..80a3eb40120 --- /dev/null +++ b/internal/olm/operator/internal/registry.go @@ -0,0 +1,98 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "context" + "fmt" + + olmresourceclient "github.com/operator-framework/operator-sdk/internal/olm/client" + registryutil "github.com/operator-framework/operator-sdk/internal/util/operator-registry" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/types" +) + +// SDKLabels are used to identify certain operator-sdk resources. +var SDKLabels = map[string]string{ + "owner": "operator-sdk", +} + +// RegistryResources configures creation/deletion of internal registry-related +// resources. +type RegistryResources struct { + Client *olmresourceclient.Client + Manifests registryutil.ManifestsStore +} + +// FEAT(estroz): allow users to specify labels for registry objects. + +// CreateRegistryManifests creates all registry objects required to serve +// manifests from m.manifests in namespace. +func (m *RegistryResources) CreateRegistryManifests(ctx context.Context, namespace string) error { + pkg := m.Manifests.GetPackageManifest() + pkgName := pkg.PackageName + bundles := m.Manifests.GetBundles() + binaryKeyValues, err := createConfigMapBinaryData(pkg, bundles) + if err != nil { + return errors.Wrap(err, "error creating registry ConfigMap binary data") + } + cm := newRegistryConfigMap(pkgName, namespace, + withBinaryData(binaryKeyValues), + ) + volName := getRegistryVolumeName(pkgName) + dep := newRegistryDeployment(pkgName, namespace, + withRegistryGRPCContainer(pkgName), + withVolumeConfigMap(volName, cm.GetName()), + withContainerVolumeMounts(volName, []string{containerManifestsDir}), + ) + service := newRegistryService(pkgName, namespace, + withTCPPort("grpc", registryGRPCPort), + ) + if err = m.Client.DoCreate(ctx, cm, dep, service); err != nil { + return errors.Wrapf(err, "error creating operator %q registry-server objects", pkgName) + } + depKey := types.NamespacedName{ + Name: dep.GetName(), + Namespace: namespace, + } + log.Infof("Waiting for Deployment %q rollout to complete", depKey) + if err = m.Client.DoRolloutWait(ctx, depKey); err != nil { + return errors.Wrapf(err, "error waiting for Deployment %q to roll out", depKey) + } + return nil +} + +// DeleteRegistryManifests deletes all registry objects serving manifests +// from m.manifests in namespace. +func (m *RegistryResources) DeleteRegistryManifests(ctx context.Context, namespace string) error { + pkgName := m.Manifests.GetPackageManifest().PackageName + cm := newRegistryConfigMap(pkgName, namespace) + dep := newRegistryDeployment(pkgName, namespace) + service := newRegistryService(pkgName, namespace) + err := m.Client.DoDelete(ctx, dep, cm, service) + if err != nil { + return errors.Wrapf(err, "error deleting operator %q registry-server objects", pkgName) + } + return nil +} + +// GetRegistryServiceAddr returns a Service's DNS name + port for a given +// pkgName and namespace. +func GetRegistryServiceAddr(pkgName, namespace string) string { + name := getRegistryServerName(pkgName) + return fmt.Sprintf("%s.%s.svc.cluster.local:%d", name, namespace, registryGRPCPort) +} diff --git a/internal/olm/operator/internal/service.go b/internal/olm/operator/internal/service.go new file mode 100644 index 00000000000..064815a2f3d --- /dev/null +++ b/internal/olm/operator/internal/service.go @@ -0,0 +1,57 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// withTCPPort returns a function that appends a service port to a Service's +// port list with name and TCP port portNum. +func withTCPPort(name string, portNum int32) func(*corev1.Service) { + return func(service *corev1.Service) { + service.Spec.Ports = append(service.Spec.Ports, corev1.ServicePort{ + Name: name, + Protocol: corev1.ProtocolTCP, + Port: portNum, + TargetPort: intstr.FromInt(int(portNum)), + }) + } +} + +// newRegistryService creates a new Service with a name derived from pkgName +// the package manifest's packageName, in namespace. The Service is created +// with labels derived from pkgName. opts will be applied to the Service object. +func newRegistryService(pkgName, namespace string, opts ...func(*corev1.Service)) *corev1.Service { + service := &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getRegistryServerName(pkgName), + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Selector: getRegistryDeploymentLabels(pkgName), + }, + } + for _, opt := range opts { + opt(service) + } + return service +} diff --git a/internal/olm/operator/manager.go b/internal/olm/operator/manager.go new file mode 100644 index 00000000000..bc24caaf901 --- /dev/null +++ b/internal/olm/operator/manager.go @@ -0,0 +1,382 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "context" + "fmt" + "io/ioutil" + + olmresourceclient "github.com/operator-framework/operator-sdk/internal/olm/client" + opinternal "github.com/operator-framework/operator-sdk/internal/olm/operator/internal" + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" + registryutil "github.com/operator-framework/operator-sdk/internal/util/operator-registry" + "github.com/operator-framework/operator-sdk/internal/util/yamlutil" + + olmapiv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1" + olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + apiextinstall "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" +) + +// TODO(estroz): ensure OLM errors are percolated up to the user. + +var ( + Scheme = olmresourceclient.Scheme + + defaultNamespace = "default" +) + +func init() { + apiextinstall.Install(Scheme) + if err := olmapiv1.AddToScheme(Scheme); err != nil { + log.Fatalf("Failed to add OLM operator API v1 types to scheme: %v", err) + } +} + +type operatorManager struct { + client *olmresourceclient.Client + version string + namespace string + force bool + + installMode olmapiv1alpha1.InstallModeType + installModeNamespaces []string + olmObjects []runtime.Object + manifests registryutil.ManifestsStore +} + +func (c *OLMCmd) newManager() (*operatorManager, error) { + m := &operatorManager{ + force: c.Force, + version: c.OperatorVersion, + } + rc, ns, err := k8sutil.GetKubeconfigAndNamespace(c.KubeconfigPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to get namespace from kubeconfig %s", c.KubeconfigPath) + } + if ns == "" { + ns = defaultNamespace + } + if c.OperatorNamespace == "" { + m.namespace = ns + } else { + m.namespace = c.OperatorNamespace + } + if m.client == nil { + m.client, err = olmresourceclient.ClientForConfig(rc) + if err != nil { + return nil, errors.Wrap(err, "failed to create SDK OLM client") + } + } + for _, path := range c.IncludePaths { + if path != "" { + objs, err := readObjectsFromFile(path) + if err != nil { + return nil, err + } + for _, obj := range objs { + m.olmObjects = append(m.olmObjects, obj) + } + } + } + // Since a Subscription refers to a CatalogSource, supplying one but + // not the other is an error. + hasSub, hasCatSrc := m.hasSubscription(), m.hasCatalogSource() + if hasSub || hasCatSrc && !(hasSub && hasCatSrc) { + return nil, errors.New("both a CatalogSource and Subscription must be supplied if one is supplied") + } + m.manifests, err = registryutil.ManifestsStoreForDir(c.ManifestsDir) + if err != nil { + return nil, err + } + if c.InstallMode == "" { + // Default to OwnNamespace. + m.installMode = olmapiv1alpha1.InstallModeTypeOwnNamespace + m.installModeNamespaces = []string{m.namespace} + } else { + m.installMode, m.installModeNamespaces, err = parseInstallModeKV(c.InstallMode) + if err != nil { + return nil, err + } + } + if err := m.installModeCompatible(m.installMode); err != nil { + return nil, err + } + return m, nil +} + +func readObjectsFromFile(path string) (objs []*unstructured.Unstructured, err error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + dec := serializer.NewCodecFactory(Scheme).UniversalDeserializer() + scanner := yamlutil.NewYAMLScanner(b) + for scanner.Scan() { + u := unstructured.Unstructured{} + _, _, err := dec.Decode(scanner.Bytes(), nil, &u) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode object from manifest %s", path) + } + objs = append(objs, &u) + } + if scanner.Err() != nil { + return nil, errors.Wrapf(scanner.Err(), "failed to scan manifest %s", path) + } + if len(objs) == 0 { + return nil, errors.Errorf("no objects found in manifest %s", path) + } + return objs, nil +} + +func (m operatorManager) hasCatalogSource() bool { + return m.hasKind(olmapiv1alpha1.CatalogSourceKind) +} + +func (m operatorManager) hasSubscription() bool { + return m.hasKind(olmapiv1alpha1.SubscriptionKind) +} + +func (m operatorManager) hasOperatorGroup() bool { + return m.hasKind(olmapiv1.OperatorGroupKind) +} + +func (m operatorManager) hasKind(kind string) bool { + for _, obj := range m.olmObjects { + if obj.GetObjectKind().GroupVersionKind().Kind == kind { + return true + } + } + return false +} + +func (m *operatorManager) up(ctx context.Context) (err error) { + // Ensure OLM is installed. + olmVer, err := m.client.GetInstalledVersion(ctx) + if err != nil { + return err + } + pkg := m.manifests.GetPackageManifest() + pkgName := pkg.PackageName + bundle, err := m.manifests.GetBundleForVersion(m.version) + if err != nil { + return err + } + csv, err := bundle.ClusterServiceVersion() + if err != nil { + return err + } + if !m.force { + // Only check CSV here, since other deployed operators/versions may be + // running with shared CRDs. + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(csv) + if err != nil { + return err + } + u := unstructured.Unstructured{Object: obj} + status := m.status(ctx, &u) + if installed, err := status.HasInstalledResources(); installed { + return errors.Errorf("an operator with name %q is already running\n%s", pkgName, status) + } else if err != nil { + return errors.Errorf("an operator with name %q is present and has resource errors\n%s", pkgName, status) + } + } + + log.Info("Creating resources") + if err = m.registryUp(ctx, olmresourceclient.OLMNamespace); err != nil { + return err + } + if !m.hasCatalogSource() { + registryGRPCAddr := opinternal.GetRegistryServiceAddr(pkgName, olmresourceclient.OLMNamespace) + catsrc := newCatalogSource(pkgName, m.namespace, withGRPC(registryGRPCAddr)) + m.olmObjects = append(m.olmObjects, catsrc) + } + if !m.hasSubscription() { + channel, err := getChannelForCSVName(pkg, csv.GetName()) + if err != nil { + return err + } + sub := newSubscription(csv.GetName(), m.namespace, + withPackageChannel(pkgName, channel), + withCatalogSource(getCatalogSourceName(pkgName), m.namespace)) + m.olmObjects = append(m.olmObjects, sub) + } + if !m.hasOperatorGroup() { + if err = m.operatorGroupUp(ctx); err != nil { + return err + } + } + // Check for Namespace objects and create those first. + namespaces, objects := []runtime.Object{}, []runtime.Object{} + for _, obj := range m.olmObjects { + if obj.GetObjectKind().GroupVersionKind().Kind == "Namespace" { + namespaces = append(namespaces, obj) + } else { + objects = append(objects, obj) + } + } + if err = m.client.DoCreate(ctx, namespaces...); err != nil { + return err + } + if err = m.client.DoCreate(ctx, objects...); err != nil { + return err + } + nn := types.NamespacedName{ + Name: csv.GetName(), + Namespace: m.namespace, + } + log.Printf("Waiting for ClusterServiceVersion %q to reach 'Succeeded' phase", nn) + if err = m.client.DoCSVWait(ctx, nn); err != nil { + return err + } + + status := m.status(ctx, bundle.Objects...) + if len(status.Resources) != len(bundle.Objects) { + return errors.Errorf("some operator %q resources did not install\n%s", csv.GetName(), status) + } + log.Infof("Successfully installed %q on OLM version %q", csv.GetName(), olmVer) + fmt.Print(status) + + return nil +} + +func (m operatorManager) registryUp(ctx context.Context, namespace string) error { + rr := opinternal.RegistryResources{ + Client: m.client, + Manifests: m.manifests, + } + registryStale, err := rr.IsManifestDataStale(ctx, namespace) + if err != nil { + if !apierrors.IsNotFound(err) { + return errors.Wrap(err, "error checking registry data") + } + // ConfigMap doesn't exist yet, so create it as usual. + if err = rr.CreateRegistryManifests(ctx, namespace); err != nil { + return errors.Wrap(err, "error registering bundle") + } + return nil + } + if !registryStale && !m.force { + log.Printf("Registry data is current") + return nil + } + if m.force { + log.Printf("Forcefully recreating registry") + } else { + log.Printf("Registry data stale. Recreating registry") + } + if err = rr.DeleteRegistryManifests(ctx, namespace); err != nil { + return errors.Wrap(err, "error deleting registered bundle") + } + if err = rr.CreateRegistryManifests(ctx, namespace); err != nil { + return errors.Wrap(err, "error registering bundle") + } + return nil +} + +func (m *operatorManager) down(ctx context.Context) (err error) { + // Ensure OLM is installed. + olmVer, err := m.client.GetInstalledVersion(ctx) + if err != nil { + return err + } + pkg := m.manifests.GetPackageManifest() + pkgName := pkg.PackageName + bundle, err := m.manifests.GetBundleForVersion(m.version) + if err != nil { + return err + } + csv, err := bundle.ClusterServiceVersion() + if err != nil { + return err + } + if !m.force { + status := m.status(ctx, bundle.Objects...) + if installed, err := status.HasInstalledResources(); !installed { + return errors.Errorf("no operator with name %q is running", pkgName) + } else if err != nil { + return errors.Errorf("an operator with name %q is present and has resource errors\n%s", pkgName, status) + } + } + + log.Info("Deleting resources") + if err = m.registryDown(ctx, olmresourceclient.OLMNamespace); err != nil { + return err + } + if !m.hasCatalogSource() { + m.olmObjects = append(m.olmObjects, newCatalogSource(pkgName, m.namespace)) + } + if !m.hasSubscription() { + m.olmObjects = append(m.olmObjects, newSubscription(csv.GetName(), m.namespace)) + } + if !m.hasOperatorGroup() { + if err = m.operatorGroupDown(ctx); err != nil { + return err + } + } + toDelete := make([]runtime.Object, len(m.olmObjects)) + copy(toDelete, m.olmObjects) + for _, o := range bundle.Objects { + oc := o.DeepCopy() + oc.SetNamespace(m.namespace) + toDelete = append(toDelete, oc) + } + if err = m.client.DoDelete(ctx, toDelete...); err != nil { + return err + } + + status := m.status(ctx, bundle.Objects...) + if installed, err := status.HasInstalledResources(); installed { + return errors.Errorf("an operator with name %q still exists", pkgName) + } else if err != nil { + return errors.Errorf("an operator with name %q is present and has resource errors\n%s", pkgName, status) + } + log.Infof("Successfully uninstalled %q on OLM version %q", csv.GetName(), olmVer) + + return nil +} + +func (m *operatorManager) registryDown(ctx context.Context, namespace string) error { + rr := opinternal.RegistryResources{ + Client: m.client, + Manifests: m.manifests, + } + if m.force { + log.Printf("Forcefully deleting registry") + if err := rr.DeleteRegistryManifests(ctx, namespace); err != nil { + return errors.Wrap(err, "error deleting registered bundle") + } + } + return nil +} + +// TODO(estroz): "status" subcommand +// TODO(estroz): check registry health on each "status" subcommand invokation +func (m *operatorManager) status(ctx context.Context, us ...*unstructured.Unstructured) olmresourceclient.Status { + objs := []runtime.Object{} + for _, u := range us { + uc := u.DeepCopy() + uc.SetNamespace(m.namespace) + objs = append(objs, uc) + } + return m.client.GetObjectsStatus(ctx, objs...) +} diff --git a/internal/olm/operator/olm.go b/internal/olm/operator/olm.go new file mode 100644 index 00000000000..4a9c0dbdcdc --- /dev/null +++ b/internal/olm/operator/olm.go @@ -0,0 +1,163 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "fmt" + + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" + + olmapiv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1" + olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-registry/pkg/registry" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// General OperatorGroup for operators created with the SDK. +const sdkOperatorGroupName = "operator-sdk-og" + +func getSubscriptionName(csvName string) string { + name := k8sutil.FormatOperatorNameDNS1123(csvName) + return fmt.Sprintf("%s-sub", name) +} + +// getChannelForCSVName returns the channel for a given csvName. csvName +// has the format "{operator-name}.(v)?{X.Y.Z}". An error is returned if +// no channel with current CSV name csvName is found. +func getChannelForCSVName(pkg registry.PackageManifest, csvName string) (registry.PackageChannel, error) { + for _, c := range pkg.Channels { + if c.CurrentCSVName == csvName { + return c, nil + } + } + return registry.PackageChannel{}, errors.Errorf("no channel in package manifest %s exists for CSV %s", pkg.PackageName, csvName) +} + +// withCatalogSource returns a function that sets the Subscription argument's +// target CatalogSource's name and namespace. +func withCatalogSource(csName, csNamespace string) func(*olmapiv1alpha1.Subscription) { + return func(sub *olmapiv1alpha1.Subscription) { + sub.Spec.CatalogSource = csName + sub.Spec.CatalogSourceNamespace = csNamespace + } +} + +// withPackageChannel returns a function that sets the Subscription argument's +// target package, channel, and starting CSV to those in channel. +func withPackageChannel(pkgName string, channel registry.PackageChannel) func(*olmapiv1alpha1.Subscription) { + return func(sub *olmapiv1alpha1.Subscription) { + if sub.Spec == nil { + sub.Spec = &olmapiv1alpha1.SubscriptionSpec{} + } + sub.Spec.Package = pkgName + sub.Spec.Channel = channel.Name + sub.Spec.StartingCSV = channel.CurrentCSVName + } +} + +// newSubscription creates a new Subscription for a CSV with a name derived +// from csvName, the CSV's objectmeta.name, in namespace. opts will be applied +// to the Subscription object. +func newSubscription(csvName, namespace string, opts ...func(*olmapiv1alpha1.Subscription)) *olmapiv1alpha1.Subscription { + sub := &olmapiv1alpha1.Subscription{ + TypeMeta: metav1.TypeMeta{ + APIVersion: olmapiv1alpha1.SchemeGroupVersion.String(), + Kind: olmapiv1alpha1.SubscriptionKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getSubscriptionName(csvName), + Namespace: namespace, + }, + } + for _, opt := range opts { + opt(sub) + } + return sub +} + +func getCatalogSourceName(pkgName string) string { + name := k8sutil.FormatOperatorNameDNS1123(pkgName) + return fmt.Sprintf("%s-ocs", name) +} + +// withGRPC returns a function that sets the CatalogSource argument's +// server type to GRPC and address at addr. +func withGRPC(addr string) func(*olmapiv1alpha1.CatalogSource) { + return func(catsrc *olmapiv1alpha1.CatalogSource) { + catsrc.Spec.SourceType = olmapiv1alpha1.SourceTypeGrpc + catsrc.Spec.Address = addr + } +} + +// newCatalogSource creates a new CatalogSource with a name derived from +// pkgName, the package manifest's packageName, in namespace. opts will +// be applied to the CatalogSource object. +func newCatalogSource(pkgName, namespace string, opts ...func(*olmapiv1alpha1.CatalogSource)) *olmapiv1alpha1.CatalogSource { + cs := &olmapiv1alpha1.CatalogSource{ + TypeMeta: metav1.TypeMeta{ + APIVersion: olmapiv1alpha1.SchemeGroupVersion.String(), + Kind: olmapiv1alpha1.CatalogSourceKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: getCatalogSourceName(pkgName), + Namespace: namespace, + }, + Spec: olmapiv1alpha1.CatalogSourceSpec{ + DisplayName: pkgName, + Publisher: "operator-sdk", + }, + } + for _, opt := range opts { + opt(cs) + } + return cs +} + +// withGRPC returns a function that sets the OperatorGroup argument's +// targetNamespaces to namespaces. namespaces can be length 0..N; if +// namespaces length is 0, targetNamespaces is set to an empty string, +// indicating a global scope. +func withTargetNamespaces(namespaces ...string) func(*olmapiv1.OperatorGroup) { + return func(og *olmapiv1.OperatorGroup) { + if len(namespaces) == 0 { + // Supports all namespaces. + og.Spec.TargetNamespaces = []string{""} + } else { + og.Spec.TargetNamespaces = namespaces + } + } +} + +// newSDKOperatorGroup creates a new OperatorGroup with name +// sdkOperatorGroupName in namespace. opts will be applied to the +// OperatorGroup object. Note that the default OperatorGroup has a global +// scope. +func newSDKOperatorGroup(namespace string, opts ...func(*olmapiv1.OperatorGroup)) *olmapiv1.OperatorGroup { + og := &olmapiv1.OperatorGroup{ + TypeMeta: metav1.TypeMeta{ + APIVersion: olmapiv1.SchemeGroupVersion.String(), + Kind: olmapiv1.OperatorGroupKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: sdkOperatorGroupName, + Namespace: namespace, + }, + } + for _, opt := range opts { + opt(og) + } + return og +} diff --git a/internal/olm/operator/operator.go b/internal/olm/operator/operator.go new file mode 100644 index 00000000000..7b3a7eb2642 --- /dev/null +++ b/internal/olm/operator/operator.go @@ -0,0 +1,147 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/spf13/pflag" +) + +// TODO(estroz): figure out a good way to deal with creating scorecard objects +// and injecting proxy container + +const ( + defaultTimeout = time.Minute * 2 +) + +// OLMCmd configures deployment and teardown of an operator via an OLM +// installation existing on a cluster. +type OLMCmd struct { + // ManifestsDir is a directory containing a package manifest and N bundles + // of the operator's CSV and CRD's. OperatorVersion can be set to the + // version of the desired operator version's subdir and Up()/Down() will + // deploy the operator version in that subdir. + ManifestsDir string + // OperatorVersion is the version of the operator to deploy. It must be + // a semantic version, ex. 0.0.1. + OperatorVersion string + // IncludePaths are path to manifests of Kubernetes resources that either + // supplement or override defaults generated by methods of OLMCmd. These + // manifests can be but are not limited to: RBAC, Subscriptions, + // CatalogSources, OperatorGroups. + // + // Kinds that are overridden if supplied: + // - CatalogSource + // - Subscription + // - OperatorGroup + IncludePaths []string + // InstallMode specifies which supported installMode should be used to + // create an OperatorGroup. The format for this field is as follows: + // + // "InstallModeType=[ns1,ns2[, ...]]" + // + // The InstallModeType string passed must be marked as "supported" in the + // CSV being installed. The namespaces passed must exist or be created by + // passing a Namespace manifest to IncludePaths. An empty set of namespaces + // can be used for AllNamespaces. + // The default mode is OwnNamespace, which uses OperatorNamespace or the + // kubeconfig default. + InstallMode string + + // KubeconfigPath is the local path to a kubeconfig. This uses well-defined + // default loading rules to load the config if empty. + KubeconfigPath string + // OperatorNamespace is the cluster namespace in which operator resources + // are created. + // OperatorNamespace must already exist in the cluster or be defined in + // a manifest passed to IncludePaths. + OperatorNamespace string + // Timeout dictates how long to wait for a REST call to complete. A call + // exceeding Timeout will generate an error. + Timeout time.Duration + // Force forces overwriting/deleting of all resources, including registries, + // respectively + Force bool + + once sync.Once +} + +var installModeFormat = "InstallModeType=[ns1,ns2[, ...]]" + +func (c *OLMCmd) AddToFlagSet(fs *pflag.FlagSet) { + fs.StringVar(&c.OperatorVersion, "operator-version", "", "Version of operator to deploy") + fs.StringVar(&c.InstallMode, "install-mode", "", "InstallMode to create OperatorGroup with. Format: "+installModeFormat) + fs.StringVar(&c.KubeconfigPath, "kubeconfig", "", "Path to kubeconfig") + fs.StringVar(&c.OperatorNamespace, "namespace", "", "Namespace in which to create resources") + fs.StringSliceVar(&c.IncludePaths, "include", nil, "Path to Kubernetes resource manifests, ex. Role, Subscription. These supplement or override defaults generated by up/down") + fs.DurationVar(&c.Timeout, "timeout", defaultTimeout, "Time to wait for the command to complete before failing") + fs.BoolVar(&c.Force, "force", false, "Force operator-sdk up/down to overwrite/delete all resources known to the command, respectively") +} + +func (c *OLMCmd) validate() error { + if c.ManifestsDir == "" { + return fmt.Errorf("manifests dir must be set") + } + if c.OperatorVersion == "" { + return fmt.Errorf("operator version must be set") + } + if c.InstallMode != "" { + if _, _, err := parseInstallModeKV(c.InstallMode); err != nil { + return err + } + } + return nil +} + +func (c *OLMCmd) initialize() { + c.once.Do(func() { + if c.Timeout <= 0 { + c.Timeout = defaultTimeout + } + }) +} + +func (c *OLMCmd) Up() error { + c.initialize() + if err := c.validate(); err != nil { + return errors.Wrapf(err, "validation error") + } + m, err := c.newManager() + if err != nil { + return errors.Wrap(err, "error initializing operator manager") + } + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout) + defer cancel() + return m.up(ctx) +} + +func (c *OLMCmd) Down() (err error) { + c.initialize() + if err := c.validate(); err != nil { + return errors.Wrapf(err, "validation error") + } + m, err := c.newManager() + if err != nil { + return errors.Wrap(err, "error initializing operator manager") + } + ctx, cancel := context.WithTimeout(context.Background(), c.Timeout) + defer cancel() + return m.down(ctx) +} diff --git a/internal/olm/operator/tenancy.go b/internal/olm/operator/tenancy.go new file mode 100644 index 00000000000..15506e29943 --- /dev/null +++ b/internal/olm/operator/tenancy.go @@ -0,0 +1,286 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package olm + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "os" + "reflect" + "sort" + "strings" + + registryutil "github.com/operator-framework/operator-sdk/internal/util/operator-registry" + + olmapiv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1" + olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // Annotation containing a CSV's member OperatorGroup's name. + olmOperatorGroupAnnotation = "olm.operatorGroup" + // Annotation containing a CSV's member OperatorGroup's namespace. + olmOperatorGroupNamespaceAnnotation = "olm.operatorNamespace" +) + +// operatorGroupDown handles logic to decide whether the SDK-managed +// OperatorGroup can be created. +// +// Check if an OperatorGroup needs to be created in m.namespace first. If +// there is and another OpreatorGroup is created, CSV installation will fail +// with reason TooManyOperatorGroups. If the CSV's installModes don't support +// the target namespace selection of the OperatorGroup, the CSV will fail +// with UnsupportedOperatorGroup. +// +// https://github.com/operator-framework/operator-lifecycle-manager/blob/master/doc/design/operatorgroups.md +func (m *operatorManager) operatorGroupUp(ctx context.Context) error { + og, err := m.getOperatorGroupInNamespace(ctx, m.namespace) + if err != nil { + return err + } + sdkOG := newSDKOperatorGroup(m.namespace, + withTargetNamespaces(m.installModeNamespaces...)) + if og == nil { + m.olmObjects = append(m.olmObjects, sdkOG) + } else { + // An exactly matching set of namespaces is needed to ensure the operator + // is deployed only in namespaces specified by the user. + sort.Strings(og.Status.Namespaces) + sort.Strings(m.installModeNamespaces) + if reflect.DeepEqual(og.Status.Namespaces, m.installModeNamespaces) { + log.Printf(" Using existing OperatorGroup %q in namespace %q", sdkOperatorGroupName, m.namespace) + } else if og.GetName() == sdkOperatorGroupName { + // operator-sdk manages this OperatorGroup, so we can modify it. + ogCSVs, err := m.getCSVsInOperatorGroup(ctx, og.GetName(), m.namespace) + if err != nil { + return err + } + if len(ogCSVs) != 0 { + fmt.Printf("OperatorGroup %q in namespace %q has existing member CSVs. "+ + "You may merge currently selected namespaces (1) with new namespaces (2):\n"+ + "(1) %+q\n(2) %+q\n"+ + "Doing so may transition installed CSVs into a Failed state with reason "+ + "UnsupportedOperatorGroup or have other unwanted side-effects.\n"+ + "Proceed? [y/N] ", og.GetName(), m.namespace, og.Status.Namespaces, + m.installModeNamespaces) + cli := bufio.NewReader(os.Stdin) + resp, err := cli.ReadString('\n') + if err != nil { + return err + } + if resp = strings.TrimSpace(resp); resp != "y" && resp != "Y" { + fmt.Printf("Not merging namespaces. Please modify the existing OperatorGroup "+ + "%q in namespace %q manually, or create this operator in a new namespace.\n", + og.GetName(), m.namespace) + os.Exit(0) + } + // All namespaces are used. + sdkOG.Spec.TargetNamespaces = mergeNamespaces( + sdkOG.Spec.TargetNamespaces, og.Status.Namespaces) + } + // Simple overwrite patch. Use merge patch type to avoid having to + // construct a JSON patch. + data, err := json.Marshal(sdkOG) + if err != nil { + return err + } + patch := client.ConstantPatch(types.MergePatchType, data) + log.Printf(" Patching less permissive OperatorGroup %q in namespace %q", sdkOperatorGroupName, m.namespace) + // Overwrite existing set of namespaces. + err = m.client.KubeClient.Patch(ctx, sdkOG, patch) + if err != nil { + return err + } + } else { + // operator-sdk does not own this OperatorGroup, cannot modify. + return errors.Errorf("existing OperatorGroup %q in namespace %q does not"+ + " select all namespaces in %+q", og.GetName(), m.namespace, m.installModeNamespaces) + } + } + return nil +} + +func mergeNamespaces(set1, set2 []string) (result []string) { + allNS := map[string]struct{}{} + for _, ns := range append(set1, set2...) { + if _, ok := allNS[ns]; !ok { + result = append(result, ns) + allNS[ns] = struct{}{} + } + } + return result +} + +// operatorGroupDown handles logic to decide whether the SDK-managed +// OperatorGroup can be deleted. +func (m *operatorManager) operatorGroupDown(ctx context.Context) error { + // Check if OperatorGroup was created by operator-sdk before + // deleting. We do not want to delete a pre-existing OperatorGroup, or + // one that is used by existing CSVs. + og, err := m.getOperatorGroupInNamespace(ctx, m.namespace) + if err != nil { + return err + } + if og != nil && og.GetName() == sdkOperatorGroupName { + ogCSVs, err := m.getCSVsInOperatorGroup(ctx, sdkOperatorGroupName, m.namespace) + if err != nil { + return err + } + bundle, err := m.manifests.GetBundleForVersion(m.version) + if err != nil { + return err + } + csv, err := bundle.ClusterServiceVersion() + if err != nil { + return err + } + if len(ogCSVs) == 0 || (len(ogCSVs) == 1 && ogCSVs[0].GetName() == csv.GetName()) { + m.olmObjects = append(m.olmObjects, newSDKOperatorGroup(m.namespace)) + } else { + log.Infof(" Existing OperatorGroup %q in namespace %q is used by existing CSVs, skipping delete", og.GetName(), m.namespace) + } + } + return nil +} + +// getCSVsInOperatorGroup gets all CSVs that are members of the OperatorGroup +// in namespace with name ogName. If ogCSVs is empty, no CSVs are members. +func (m operatorManager) getCSVsInOperatorGroup(ctx context.Context, ogName, namespace string) (ogCSVs []*olmapiv1alpha1.ClusterServiceVersion, err error) { + csvs := olmapiv1alpha1.ClusterServiceVersionList{} + opt := client.InNamespace(namespace) + err = m.client.KubeClient.List(ctx, &csvs, opt) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + for _, csv := range csvs.Items { + annotations := csv.GetAnnotations() + if annotations != nil { + csvOGName, ogOK := annotations[olmOperatorGroupAnnotation] + csvOGNamespace, nsOK := annotations[olmOperatorGroupNamespaceAnnotation] + // TODO(estroz): ensure this works for "" (AllNamespaces). + if ogOK && nsOK && csvOGName == ogName && csvOGNamespace == namespace { + ogCSVs = append(ogCSVs, &csv) + } + } + } + return ogCSVs, nil +} + +// installModeCompatible ensures installMode is compatible with the namespaces +// and CSV's installModes being used. +func (m operatorManager) installModeCompatible(installMode olmapiv1alpha1.InstallModeType) error { + err := validateInstallModeWithNamespaces(installMode, m.installModeNamespaces) + if err != nil { + return err + } + if installMode == olmapiv1alpha1.InstallModeTypeOwnNamespace { + if ns := m.installModeNamespaces[0]; ns != m.namespace { + return errors.Errorf("installMode %s namespace %q must match namespace %q", installMode, ns, m.namespace) + } + } + // Ensure CSV supports installMode. + bundle, err := m.manifests.GetBundleForVersion(m.version) + if err != nil { + return err + } + csv, err := bundle.ClusterServiceVersion() + if err != nil { + return err + } + olmCSV := registryutil.MustBundleCSVToCSV(csv) + for _, mode := range olmCSV.Spec.InstallModes { + if mode.Type == installMode && !mode.Supported { + return errors.Errorf("installMode %s not supported in CSV %q", installMode, olmCSV.GetName()) + } + } + return nil +} + +// Mapping of installMode string values to types, for validation. +var installModeStrings = map[string]olmapiv1alpha1.InstallModeType{ + string(olmapiv1alpha1.InstallModeTypeOwnNamespace): olmapiv1alpha1.InstallModeTypeOwnNamespace, + string(olmapiv1alpha1.InstallModeTypeSingleNamespace): olmapiv1alpha1.InstallModeTypeSingleNamespace, + string(olmapiv1alpha1.InstallModeTypeMultiNamespace): olmapiv1alpha1.InstallModeTypeMultiNamespace, + string(olmapiv1alpha1.InstallModeTypeAllNamespaces): olmapiv1alpha1.InstallModeTypeAllNamespaces, +} + +// parseInstallModeKV parses an installMode string of the format +// installModeFormat. +func parseInstallModeKV(raw string) (olmapiv1alpha1.InstallModeType, []string, error) { + modeSplit := strings.Split(raw, "=") + if len(modeSplit) != 2 { + return "", nil, errors.Errorf("installMode string %q is malformatted, must be: %s", raw, installModeFormat) + } + modeStr, namespaceList := modeSplit[0], modeSplit[1] + mode, ok := installModeStrings[modeStr] + if !ok { + return "", nil, errors.Errorf("installMode type string %q is not a valid installMode type", modeStr) + } + namespaces := []string{} + for _, namespace := range strings.Split(strings.Trim(namespaceList, ","), ",") { + namespaces = append(namespaces, namespace) + } + return mode, namespaces, nil +} + +// validateInstallModeWithNamespaces ensures namespaces are valid given mode. +func validateInstallModeWithNamespaces(mode olmapiv1alpha1.InstallModeType, namespaces []string) error { + switch mode { + case olmapiv1alpha1.InstallModeTypeOwnNamespace, olmapiv1alpha1.InstallModeTypeSingleNamespace: + if len(namespaces) != 1 || namespaces[0] == "" { + return errors.Errorf("installMode %s must be passed with exactly one non-empty namespace, have: %+q", mode, namespaces) + } + case olmapiv1alpha1.InstallModeTypeMultiNamespace: + if len(namespaces) < 2 { + return errors.Errorf("installMode %s must be passed with more than one non-empty namespaces, have: %+q", mode, namespaces) + } + case olmapiv1alpha1.InstallModeTypeAllNamespaces: + if len(namespaces) != 1 || namespaces[0] != "" { + return errors.Errorf("installMode %s must be passed with exactly one empty namespace, have: %+q", mode, namespaces) + } + default: + return errors.Errorf("installMode %q is not a valid installMode type", mode) + } + return nil +} + +// getOperatorGroupInNamespace gets the OperatorGroup in namespace. Because +// there must only be one OperatorGroup per namespace, an error is returned +// if more than one is found. nil is returned if no OperatorGroup exists in +// namespace. +func (m operatorManager) getOperatorGroupInNamespace(ctx context.Context, namespace string) (*olmapiv1.OperatorGroup, error) { + // There must only be one OperatorGroup per namespace, but we should use list. + ogs := olmapiv1.OperatorGroupList{} + err := m.client.KubeClient.List(ctx, &ogs, client.InNamespace(namespace)) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + if apierrors.IsNotFound(err) || len(ogs.Items) == 0 { + return nil, nil + } + // There should never be more than one, but if there is return an error. + if len(ogs.Items) > 1 { + return nil, errors.Errorf("more than one OperatorGroup exists in namespace %q", namespace) + } + currOG := &ogs.Items[0] + return currOG, nil +} diff --git a/internal/util/k8sutil/k8sutil.go b/internal/util/k8sutil/k8sutil.go index bb934a29e5b..48ea5bcbd3f 100644 --- a/internal/util/k8sutil/k8sutil.go +++ b/internal/util/k8sutil/k8sutil.go @@ -18,12 +18,14 @@ import ( "bytes" "fmt" "io" + "regexp" "strings" "unicode" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -113,3 +115,15 @@ func GetTypeMetaFromBytes(b []byte) (t metav1.TypeMeta, err error) { Kind: u.GetKind(), }, nil } + +// dns1123LabelRegexp defines the character set allowed in a DNS 1123 label. +var dns1123LabelRegexp = regexp.MustCompile("[^a-zA-Z0-9]+") + +// FormatOperatorNameDNS1123 ensures name is DNS1123 label-compliant by +// replacing all non-compliant UTF-8 characters with "-". +func FormatOperatorNameDNS1123(name string) string { + if len(validation.IsDNS1123Label(name)) != 0 { + return dns1123LabelRegexp.ReplaceAllString(name, "-") + } + return name +} diff --git a/internal/util/operator-registry/bundle.go b/internal/util/operator-registry/bundle.go new file mode 100644 index 00000000000..5e3ed751da0 --- /dev/null +++ b/internal/util/operator-registry/bundle.go @@ -0,0 +1,163 @@ +// Copyright 2019 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "encoding/json" + + "github.com/blang/semver" + olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-registry/pkg/registry" + "github.com/operator-framework/operator-registry/pkg/sqlite" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// manifestsLoad loads a manifests directory from disk. +type manifestsLoad struct { + dir string + pkg registry.PackageManifest + bundles map[string]*registry.Bundle +} + +// Ensure manifestsLoad implements registry.Load. +var _ registry.Load = &manifestsLoad{} + +// populate uses operator-registry's sqlite.NewSQLLoaderForDirectory to load +// l.dir's manifests. Note that this method does not call any functions that +// use SQL drivers. +func (l *manifestsLoad) populate() error { + loader := sqlite.NewSQLLoaderForDirectory(l, l.dir) + if err := loader.Populate(); err != nil { + return errors.Wrapf(err, "error getting bundles from manifests dir %q", l.dir) + } + return nil +} + +// AddOperatorBundle adds a bundle to l. +func (l *manifestsLoad) AddOperatorBundle(bundle *registry.Bundle) error { + csvRaw, err := bundle.ClusterServiceVersion() + if err != nil { + return errors.Wrap(err, "error getting bundle CSV") + } + csvSpec := olmapiv1alpha1.ClusterServiceVersionSpec{} + if err := json.Unmarshal(csvRaw.Spec, &csvSpec); err != nil { + return errors.Wrap(err, "error unmarshaling CSV spec") + } + bundle.Name = csvSpec.Version.String() + l.bundles[csvSpec.Version.String()] = bundle + return nil +} + +// AddOperatorBundle adds the package manifest to l. +func (l *manifestsLoad) AddPackageChannels(pkg registry.PackageManifest) error { + l.pkg = pkg + return nil +} + +// AddBundlePackageChannels is a no-op to implement the registry.Load interface. +func (l *manifestsLoad) AddBundlePackageChannels(manifest registry.PackageManifest, bundle registry.Bundle) error { + return nil +} + +// RmPackageName is a no-op to implement the registry.Load interface. +func (l *manifestsLoad) RmPackageName(packageName string) error { + return nil +} + +// ClearNonDefaultBundles is a no-op to implement the registry.Load interface. +func (l *manifestsLoad) ClearNonDefaultBundles(packageName string) error { + return nil +} + +// ManifestsStore knows how to query for an operator's package manifest and +// related bundles. +type ManifestsStore interface { + // GetPackageManifest returns the ManifestsStore's registry.PackageManifest. + // The returned object is assumed to be valid. + GetPackageManifest() registry.PackageManifest + // GetBundles returns the ManifestsStore's set of registry.Bundle. These + // bundles are unique by CSV version, since only one operator type should + // exist in one manifests dir. + // The returned objects are assumed to be valid. + GetBundles() []*registry.Bundle + // GetBundleForVersion returns the ManifestsStore's registry.Bundle for a + // given version string. An error should be returned if the passed version + // does not exist in the store. + // The returned object is assumed to be valid. + GetBundleForVersion(string) (*registry.Bundle, error) +} + +// manifests implements ManifestsStore +type manifests struct { + pkg registry.PackageManifest + bundles map[string]*registry.Bundle +} + +// ManifestsStoreForDir populates a ManifestsStore from the metadata in dir. +// Each bundle and the package manifest are statically validated, and will +// return an error if any are not valid. +func ManifestsStoreForDir(dir string) (ManifestsStore, error) { + load := &manifestsLoad{ + dir: dir, + bundles: map[string]*registry.Bundle{}, + } + if err := load.populate(); err != nil { + return nil, err + } + return &manifests{ + pkg: load.pkg, + bundles: load.bundles, + }, nil +} + +func (l manifests) GetPackageManifest() registry.PackageManifest { + return l.pkg +} + +func (l manifests) GetBundles() (bundles []*registry.Bundle) { + for _, bundle := range l.bundles { + bundles = append(bundles, bundle) + } + return bundles +} + +func (l manifests) GetBundleForVersion(version string) (*registry.Bundle, error) { + if _, err := semver.Parse(version); err != nil { + return nil, errors.Wrapf(err, "error getting bundle for version %q", version) + } + bundle, ok := l.bundles[version] + if !ok { + return nil, errors.Errorf("bundle for version %q does not exist", version) + } + return bundle, nil +} + +// MustBundleCSVToCSV converts a registry.ClusterServiceVersion bcsv to a +// v1alpha1.ClusterServiceVersion. The returned type will not have a status. +// MustBundleCSVToCSV will exit if bcsv's Spec is incorrectly formatted, +// since operator-registry should have not been able to parse the CSV +// if it were not. +func MustBundleCSVToCSV(bcsv *registry.ClusterServiceVersion) *olmapiv1alpha1.ClusterServiceVersion { + spec := olmapiv1alpha1.ClusterServiceVersionSpec{} + if err := json.Unmarshal(bcsv.Spec, &spec); err != nil { + log.Fatalf("Error converting bundle CSV %q type: %v", bcsv.GetName(), err) + } + return &olmapiv1alpha1.ClusterServiceVersion{ + TypeMeta: bcsv.TypeMeta, + ObjectMeta: bcsv.ObjectMeta, + Spec: spec, + } +} diff --git a/internal/util/operator-registry/package_manifest.go b/internal/util/operator-registry/validate.go similarity index 79% rename from internal/util/operator-registry/package_manifest.go rename to internal/util/operator-registry/validate.go index eedd0509974..7ef451b48b4 100644 --- a/internal/util/operator-registry/package_manifest.go +++ b/internal/util/operator-registry/validate.go @@ -21,6 +21,8 @@ import ( "github.com/pkg/errors" ) +// ValidatePackageManifest ensures each datum in pkg is valid relative to other +// related data in pkg. func ValidatePackageManifest(pkg *registry.PackageManifest) error { if pkg.PackageName == "" { return errors.New("package name cannot be empty") @@ -30,7 +32,7 @@ func ValidatePackageManifest(pkg *registry.PackageManifest) error { return errors.New("channels cannot be empty") } if pkg.DefaultChannelName == "" && numChannels > 1 { - return errors.New("default channel cannot be empty") + return errors.New("default channel cannot be empty if more than one channel exists") } seen := map[string]struct{}{} @@ -39,15 +41,15 @@ func ValidatePackageManifest(pkg *registry.PackageManifest) error { return fmt.Errorf("channel %d name cannot be empty", i) } if c.CurrentCSVName == "" { - return fmt.Errorf("channel %s currentCSV cannot be empty", c.Name) + return fmt.Errorf("channel %q currentCSV cannot be empty", c.Name) } if _, ok := seen[c.Name]; ok { - return fmt.Errorf("duplicate package manifest channel name %s; channel names must be unique", c.Name) + return fmt.Errorf("duplicate package manifest channel name %q; channel names must be unique", c.Name) } seen[c.Name] = struct{}{} } if _, found := seen[pkg.DefaultChannelName]; pkg.DefaultChannelName != "" && !found { - return fmt.Errorf("default channel %s does not exist in channels", pkg.DefaultChannelName) + return fmt.Errorf("default channel %q does not exist in channels", pkg.DefaultChannelName) } return nil diff --git a/internal/util/operator-registry/package_manifest_test.go b/internal/util/operator-registry/validate_test.go similarity index 91% rename from internal/util/operator-registry/package_manifest_test.go rename to internal/util/operator-registry/validate_test.go index 5a84b967ac5..06a949acd4b 100644 --- a/internal/util/operator-registry/package_manifest_test.go +++ b/internal/util/operator-registry/validate_test.go @@ -50,7 +50,7 @@ func TestValidatePackageManifest(t *testing.T) { }, { "no default channel and more than one channel", - true, "default channel cannot be empty", + true, "default channel cannot be empty if more than one channel exists", ®istry.PackageManifest{ Channels: []registry.PackageChannel{ {Name: "foo", CurrentCSVName: "bar"}, @@ -61,7 +61,7 @@ func TestValidatePackageManifest(t *testing.T) { }, { "default channel does not exist", - true, "default channel baz does not exist in channels", + true, "default channel \"baz\" does not exist in channels", ®istry.PackageManifest{ Channels: []registry.PackageChannel{ {Name: "foo", CurrentCSVName: "bar"}, @@ -81,7 +81,7 @@ func TestValidatePackageManifest(t *testing.T) { }, { "one channel's CSVName is empty", - true, "channel foo currentCSV cannot be empty", + true, "channel \"foo\" currentCSV cannot be empty", ®istry.PackageManifest{ Channels: []registry.PackageChannel{{Name: "foo"}}, DefaultChannelName: "baz", @@ -90,7 +90,7 @@ func TestValidatePackageManifest(t *testing.T) { }, { "duplicate channel name", - true, "duplicate package manifest channel name foo; channel names must be unique", + true, "duplicate package manifest channel name \"foo\"; channel names must be unique", ®istry.PackageManifest{ Channels: []registry.PackageChannel{ {Name: "foo", CurrentCSVName: "bar"}, diff --git a/test/integration/operator_olm_test.go b/test/integration/operator_olm_test.go new file mode 100644 index 00000000000..18d81de5eab --- /dev/null +++ b/test/integration/operator_olm_test.go @@ -0,0 +1,117 @@ +// Copyright 2018 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "io/ioutil" + "os" + "testing" + "time" + + operator "github.com/operator-framework/operator-sdk/internal/olm/operator" + "github.com/operator-framework/operator-sdk/pkg/k8sutil" + + opv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" +) + +const ( + defaultTimeout = 2 * time.Minute +) + +var ( + kubeconfigPath = os.Getenv(k8sutil.KubeConfigEnvVar) +) + +func TestOLMIntegration(t *testing.T) { + if image, ok := os.LookupEnv(imageEnvVar); ok && image != "" { + defaultTestImageTag = image + } + t.Run("Operator", func(t *testing.T) { + t.Run("Single", SingleOperator) + }) +} + +func SingleOperator(t *testing.T) { + csvConfig := CSVTemplateConfig{ + OperatorName: "memcached-operator", + OperatorVersion: "0.0.2", + TestImageTag: defaultTestImageTag, + Maturity: "alpha", + ReplacesCSVName: "", + CRDKeys: []DefinitionKey{ + { + Kind: "Memcached", + Name: "memcacheds.cache.example.com", + Group: "cache.example.com", + Versions: []apiextv1beta1.CustomResourceDefinitionVersion{ + {Name: "v1alpha1", Storage: true, Served: true}, + }, + }, + }, + InstallModes: []opv1alpha1.InstallMode{ + {Type: opv1alpha1.InstallModeTypeOwnNamespace, Supported: true}, + {Type: opv1alpha1.InstallModeTypeSingleNamespace, Supported: true}, + {Type: opv1alpha1.InstallModeTypeMultiNamespace, Supported: false}, + {Type: opv1alpha1.InstallModeTypeAllNamespaces, Supported: true}, + }, + } + tmp, err := ioutil.TempDir("", "sdk-integration.") + if err != nil { + t.Fatal(err) + } + defaultChannel := "alpha" + operatorName := "memcached-operator" + operatorVersion := "0.0.2" + manifestsDir, err := writeOperatorManifests(tmp, operatorName, defaultChannel, csvConfig) + if err != nil { + os.RemoveAll(tmp) + t.Fatal(err) + } + opcmd := operator.OLMCmd{ + ManifestsDir: manifestsDir, + OperatorVersion: operatorVersion, + KubeconfigPath: kubeconfigPath, + Timeout: defaultTimeout, + } + cases := []struct { + description string + op func() error + force bool + wantErr bool + }{ + {"Remove operator before deploy", opcmd.Down, false, true}, + {"Deploy operator", opcmd.Up, false, false}, + {"Deploy operator after deploy", opcmd.Up, false, true}, + {"Deploy operator after deploy with force", opcmd.Up, true, false}, + {"Remove operator after deploy", opcmd.Down, false, false}, + {"Remove operator after removal", opcmd.Down, false, true}, + {"Remove operator after removal with force", opcmd.Down, true, false}, + } + defer func() { + opcmd.Force = true + _ = opcmd.Down() + os.RemoveAll(tmp) + }() + for _, c := range cases { + opcmd.Force = c.force + err := c.op() + if c.wantErr && err == nil { + t.Fatalf("%s (%s): wanted error, got: nil", c.description, operatorName) + } else if !c.wantErr && err != nil { + t.Fatalf("%s (%s): wanted no error, got: %v", c.description, operatorName, err) + } + } +} diff --git a/test/integration/test_suite.go b/test/integration/test_suite.go new file mode 100644 index 00000000000..4c281fc0099 --- /dev/null +++ b/test/integration/test_suite.go @@ -0,0 +1,283 @@ +// Copyright 2018 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "fmt" + "html/template" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/ghodss/yaml" + operatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-registry/pkg/registry" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + imageEnvVar = "OSDK_INTEGRATION_IMAGE" +) + +var ( + // Set with OSDK_INTEGRATION_IMAGE in CI. + defaultTestImageTag = "memcached-operator" +) + +type DefinitionKey struct { + Kind string + Name string + Group string + Versions []apiextv1beta1.CustomResourceDefinitionVersion +} + +type CSVTemplateConfig struct { + OperatorName string + OperatorVersion string + TestImageTag string + Maturity string + ReplacesCSVName string + CRDKeys []DefinitionKey + InstallModes []operatorsv1alpha1.InstallMode +} + +const csvTmpl = `apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + capabilities: Basic Install + name: {{ .OperatorName }}.v{{ .OperatorVersion }} + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: +{{- range $i, $crd := .CRDKeys }}{{- range $j, $version := $crd.Versions }} + - description: Represents a cluster of {{ $crd.Kind }} apps + displayName: {{ $crd.Kind }} App + kind: {{ $crd.Kind }} + name: {{ $crd.Name }} + resources: + - kind: Deployment + version: v1 + - kind: ReplicaSet + version: v1 + - kind: Pod + version: v1 + specDescriptors: + - description: The desired number of member Pods for the deployment. + displayName: Size + path: size + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podCount + statusDescriptors: + - description: The current status of the application. + displayName: Status + path: phase + x-descriptors: + - urn:alm:descriptor:io.kubernetes.phase + - description: Explanation for the current status of the application. + displayName: Status Details + path: reason + x-descriptors: + - urn:alm:descriptor:io.kubernetes.phase:reason + version: {{ $version.Name }} +{{- end }}{{- end }} + description: Big ol' Operator. + displayName: {{ .OperatorName }} Application + install: + spec: + deployments: + - name: {{ .OperatorName }} + spec: + replicas: 1 + selector: + matchLabels: + name: {{ .OperatorName }} + strategy: {} + template: + metadata: + labels: + name: {{ .OperatorName }} + spec: + containers: + - command: + - {{ .OperatorName }} + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.annotations['olm.targetNamespaces'] + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: {{ .OperatorName }} + image: {{ .TestImageTag }} + imagePullPolicy: Never + name: {{ .OperatorName }} + resources: {} + serviceAccountName: {{ .OperatorName }} + permissions: + - rules: + - apiGroups: + - "" + resources: + - pods + - services + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - '*' + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - '*' + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - get + - create + - apiGroups: + - apps + resourceNames: + - {{ .OperatorName }} + resources: + - deployments/finalizers + verbs: + - update + serviceAccountName: {{ .OperatorName }} + strategy: deployment + installModes: +{{- range $i, $mode := .InstallModes }} + - supported: {{ $mode.Supported }} + type: {{ $mode.Type }} +{{- end }} + keywords: + - big + - ol + - operator + maintainers: + - email: corp@example.com + name: Some Corp + maturity: {{ .Maturity }} + provider: + name: Example + url: www.example.com +{{- if .ReplacesCSVName }} + replaces: {{ .ReplacesCSVName }} +{{- end }} + version: {{ .OperatorVersion }} +` + +func writeOperatorManifests(root, operatorName, defaultChannel string, csvConfigs ...CSVTemplateConfig) (manifestsDir string, err error) { + manifestsDir = filepath.Join(root, operatorName) + pkg := registry.PackageManifest{ + PackageName: operatorName, + DefaultChannelName: defaultChannel, + } + for _, csvConfig := range csvConfigs { + pkg.Channels = append(pkg.Channels, registry.PackageChannel{ + Name: csvConfig.Maturity, + CurrentCSVName: fmt.Sprintf("%s.v%s", csvConfig.OperatorName, csvConfig.OperatorVersion), + }) + bundleDir := filepath.Join(manifestsDir, csvConfig.OperatorVersion) + for _, key := range csvConfig.CRDKeys { + crd := apiextv1beta1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiextv1beta1.SchemeGroupVersion.String(), + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{Name: key.Name}, + Spec: apiextv1beta1.CustomResourceDefinitionSpec{ + Names: apiextv1beta1.CustomResourceDefinitionNames{ + Kind: key.Kind, + ListKind: key.Kind + "List", + Singular: strings.ToLower(key.Kind), + Plural: strings.ToLower(key.Kind) + "s", + }, + Group: key.Group, + Scope: "Namespaced", + Versions: key.Versions, + }, + } + crdPath := filepath.Join(bundleDir, fmt.Sprintf("%s.crd.yaml", key.Name)) + if err = writeObjectManifest(crdPath, crd); err != nil { + return "", err + } + } + csvPath := filepath.Join(bundleDir, fmt.Sprintf("%s.v%s.csv.yaml", csvConfig.OperatorName, csvConfig.OperatorVersion)) + if err = execTemplateOnFile(csvPath, csvTmpl, csvConfig); err != nil { + return "", err + } + } + pkgPath := filepath.Join(manifestsDir, fmt.Sprintf("%s.package.yaml", operatorName)) + if err = writeObjectManifest(pkgPath, pkg); err != nil { + return "", err + } + return manifestsDir, nil +} + +func writeObjectManifest(path string, o interface{}) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + b, err := yaml.Marshal(o) + if err != nil { + return err + } + if err = ioutil.WriteFile(path, b, 0644); err != nil { + return err + } + return nil +} + +func execTemplateOnFile(path, tmpl string, o interface{}) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + w, err := os.Create(path) + if err != nil { + return err + } + defer w.Close() + csvTmpl, err := template.New(path).Parse(tmpl) + if err != nil { + return err + } + if err = csvTmpl.Execute(w, o); err != nil { + return err + } + return nil +} diff --git a/test/test-framework/build/Dockerfile b/test/test-framework/build/Dockerfile index 464c54c794a..dfee3c4c365 100644 --- a/test/test-framework/build/Dockerfile +++ b/test/test-framework/build/Dockerfile @@ -2,7 +2,7 @@ FROM registry.access.redhat.com/ubi8/ubi-minimal:latest ENV OPERATOR=/usr/local/bin/memcached-operator \ USER_UID=1001 \ - USER_NAME=test-framework + USER_NAME=memcached-operator # install operator binary COPY build/_output/bin/test-framework ${OPERATOR} diff --git a/test/test-framework/go.sum b/test/test-framework/go.sum index 4e300b8e33b..7c1a0ba0aff 100644 --- a/test/test-framework/go.sum +++ b/test/test-framework/go.sum @@ -143,6 +143,7 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libnetwork v0.0.0-20180830151422-a9cd636e3789/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -150,6 +151,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -300,8 +303,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20190203031600-7a902570cb17/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= @@ -463,6 +466,7 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/operator-framework/operator-lifecycle-manager v0.0.0-20191115003340-16619cd27fa5/go.mod h1:zL34MNy92LPutBH5gQK+gGhtgTUlZZX03I2G12vWHF4= github.com/operator-framework/operator-registry v1.5.1/go.mod h1:agrQlkWOo1q8U1SAaLSS2WQ+Z9vswNT2M2HFib9iuLY= +github.com/operator-framework/operator-registry v1.5.3/go.mod h1:agrQlkWOo1q8U1SAaLSS2WQ+Z9vswNT2M2HFib9iuLY= github.com/otiai10/copy v1.0.1/go.mod h1:8bMCJrAqOtN/d9oyh5HR7HhLQMvcGMpGdwRDYsfOCHc= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4= @@ -518,6 +522,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uY github.com/robfig/cron v0.0.0-20170526150127-736158dc09e1/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw=