Constellation Operator: Add image version field (#649)

This commit is contained in:
Malte Poll 2022-11-25 14:49:26 +01:00 committed by GitHub
parent 89b25f8ebb
commit 1af3ff00ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 496 additions and 7 deletions

View File

@ -36,6 +36,10 @@ spec:
image:
description: ImageReference is the image to use for all nodes.
type: string
imageVersion:
description: ImageVersion is the CSP independent version of the image
to use for all nodes.
type: string
type: object
status:
description: NodeImageStatus defines the observed state of NodeImage.

View File

@ -79,6 +79,10 @@ spec:
volumeMounts:
- mountPath: /etc/kubernetes/pki/etcd
name: etcd-certs
- mountPath: /host/usr/lib/os-release
name: usr-lib-os-release
- mountPath: /etc/os-release
name: etc-os-release
- mountPath: /etc/azure
name: azureconfig
readOnly: true
@ -103,6 +107,16 @@ spec:
path: /etc/kubernetes/pki/etcd
type: Directory
name: etcd-certs
- hostPath:
path: /usr/lib/os-release
type: File
optional: true
name: usr-lib-os-release
- hostPath:
path: /etc/os-release
type: File
optional: true
name: etc-os-release
- name: azureconfig
secret:
optional: true

View File

@ -97,6 +97,10 @@ spec:
volumeMounts:
- mountPath: /etc/kubernetes/pki/etcd
name: etcd-certs
- mountPath: /host/usr/lib/os-release
name: usr-lib-os-release
- mountPath: /etc/os-release
name: etc-os-release
- mountPath: /etc/azure
name: azureconfig
readOnly: true
@ -121,6 +125,16 @@ spec:
path: /etc/kubernetes/pki/etcd
type: Directory
name: etcd-certs
- hostPath:
path: /usr/lib/os-release
type: File
optional: true
name: usr-lib-os-release
- hostPath:
path: /etc/os-release
type: File
optional: true
name: etc-os-release
- name: azureconfig
secret:
optional: true

View File

@ -97,6 +97,10 @@ spec:
volumeMounts:
- mountPath: /etc/kubernetes/pki/etcd
name: etcd-certs
- mountPath: /host/usr/lib/os-release
name: usr-lib-os-release
- mountPath: /etc/os-release
name: etc-os-release
- mountPath: /etc/azure
name: azureconfig
readOnly: true
@ -121,6 +125,16 @@ spec:
path: /etc/kubernetes/pki/etcd
type: Directory
name: etcd-certs
- hostPath:
path: /usr/lib/os-release
type: File
optional: true
name: usr-lib-os-release
- hostPath:
path: /etc/os-release
type: File
optional: true
name: etc-os-release
- name: azureconfig
secret:
optional: true

View File

@ -97,6 +97,10 @@ spec:
volumeMounts:
- mountPath: /etc/kubernetes/pki/etcd
name: etcd-certs
- mountPath: /host/usr/lib/os-release
name: usr-lib-os-release
- mountPath: /etc/os-release
name: etc-os-release
- mountPath: /etc/azure
name: azureconfig
readOnly: true
@ -121,6 +125,16 @@ spec:
path: /etc/kubernetes/pki/etcd
type: Directory
name: etcd-certs
- hostPath:
path: /usr/lib/os-release
type: File
optional: true
name: usr-lib-os-release
- hostPath:
path: /etc/os-release
type: File
optional: true
name: etc-os-release
- name: azureconfig
secret:
optional: true

View File

@ -15,6 +15,8 @@ import (
type NodeImageSpec struct {
// ImageReference is the image to use for all nodes.
ImageReference string `json:"image,omitempty"`
// ImageVersion is the CSP independent version of the image to use for all nodes.
ImageVersion string `json:"imageVersion,omitempty"`
}
// NodeImageStatus defines the observed state of NodeImage.

View File

@ -38,6 +38,10 @@ spec:
image:
description: ImageReference is the image to use for all nodes.
type: string
imageVersion:
description: ImageVersion is the CSP independent version of the image
to use for all nodes.
type: string
type: object
status:
description: NodeImageStatus defines the observed state of NodeImage.

View File

@ -50,6 +50,10 @@ spec:
volumeMounts:
- mountPath: /etc/kubernetes/pki/etcd
name: etcd-certs
- mountPath: /host/usr/lib/os-release
name: usr-lib-os-release
- mountPath: /etc/os-release
name: etc-os-release
- mountPath: /etc/azure
name: azureconfig
readOnly: true
@ -68,6 +72,16 @@ spec:
hostPath:
path: /etc/kubernetes/pki/etcd
type: Directory
- hostPath:
path: /usr/lib/os-release
type: File
optional: true
name: usr-lib-os-release
- hostPath:
path: /etc/os-release
type: File
optional: true
name: etc-os-release
- name: azureconfig
secret:
secretName: azureconfig

View File

@ -18,10 +18,12 @@ import (
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// InitialResources creates the initial resources for the node operator.
func InitialResources(ctx context.Context, k8sClient client.Writer, scalingGroupGetter scalingGroupGetter, uid string) error {
func InitialResources(ctx context.Context, k8sClient client.Writer, imageInfo imageInfoGetter, scalingGroupGetter scalingGroupGetter, uid string) error {
logr := log.FromContext(ctx)
controlPlaneGroupIDs, workerGroupIDs, err := scalingGroupGetter.ListScalingGroups(ctx, uid)
if err != nil {
return fmt.Errorf("listing scaling groups: %w", err)
@ -40,7 +42,15 @@ func InitialResources(ctx context.Context, k8sClient client.Writer, scalingGroup
if err != nil {
return fmt.Errorf("determining initial node image: %w", err)
}
if err := createNodeImage(ctx, k8sClient, imageReference); err != nil {
imageVersion, err := imageInfo.ImageVersion(imageReference)
if err != nil {
// do not fail if the image version cannot be determined
// this is important for backwards compatibility
logr.Error(err, "determining initial node image version")
imageVersion = ""
}
if err := createNodeImage(ctx, k8sClient, imageReference, imageVersion); err != nil {
return fmt.Errorf("creating initial node image %q: %w", imageReference, err)
}
for _, groupID := range controlPlaneGroupIDs {
@ -101,7 +111,7 @@ func createAutoscalingStrategy(ctx context.Context, k8sClient client.Writer, pro
}
// createNodeImage creates the initial nodeimage resource if it does not exist yet.
func createNodeImage(ctx context.Context, k8sClient client.Writer, imageReference string) error {
func createNodeImage(ctx context.Context, k8sClient client.Writer, imageReference, imageVersion string) error {
err := k8sClient.Create(ctx, &updatev1alpha1.NodeImage{
TypeMeta: metav1.TypeMeta{APIVersion: "update.edgeless.systems/v1alpha1", Kind: "NodeImage"},
ObjectMeta: metav1.ObjectMeta{
@ -109,6 +119,7 @@ func createNodeImage(ctx context.Context, k8sClient client.Writer, imageReferenc
},
Spec: updatev1alpha1.NodeImageSpec{
ImageReference: imageReference,
ImageVersion: imageVersion,
},
})
if k8sErrors.IsAlreadyExists(err) {
@ -139,6 +150,10 @@ func createScalingGroup(ctx context.Context, config newScalingGroupConfig) error
return err
}
type imageInfoGetter interface {
ImageVersion(imageReference string) (string, error)
}
type scalingGroupGetter interface {
// GetScalingGroupImage retrieves the image currently used by a scaling group.
GetScalingGroupImage(ctx context.Context, scalingGroupID string) (string, error)

View File

@ -87,7 +87,7 @@ func TestInitialResources(t *testing.T) {
k8sClient := &stubK8sClient{createErr: tc.createErr}
scalingGroupGetter := newScalingGroupGetter(tc.items, tc.imageErr, tc.nameErr, tc.listErr)
err := InitialResources(context.Background(), k8sClient, scalingGroupGetter, "uid")
err := InitialResources(context.Background(), k8sClient, &stubImageInfo{}, scalingGroupGetter, "uid")
if tc.wantErr {
assert.Error(err)
return
@ -183,6 +183,7 @@ func TestCreateNodeImage(t *testing.T) {
},
Spec: updatev1alpha1.NodeImageSpec{
ImageReference: "image-reference",
ImageVersion: "image-version",
},
},
},
@ -199,6 +200,7 @@ func TestCreateNodeImage(t *testing.T) {
},
Spec: updatev1alpha1.NodeImageSpec{
ImageReference: "image-reference",
ImageVersion: "image-version",
},
},
},
@ -210,7 +212,7 @@ func TestCreateNodeImage(t *testing.T) {
require := require.New(t)
k8sClient := &stubK8sClient{createErr: tc.createErr}
err := createNodeImage(context.Background(), k8sClient, "image-reference")
err := createNodeImage(context.Background(), k8sClient, "image-reference", "image-version")
if tc.wantErr {
assert.Error(err)
return
@ -297,6 +299,15 @@ func (s *stubK8sClient) Create(ctx context.Context, obj client.Object, opts ...c
return s.createErr
}
type stubImageInfo struct {
imageVersion string
err error
}
func (s stubImageInfo) ImageVersion(_ string) (string, error) {
return s.imageVersion, s.err
}
type stubScalingGroupGetter struct {
store map[string]scalingGroupStoreItem
imageErr error

View File

@ -0,0 +1,154 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package deploy
import (
"bufio"
"fmt"
"regexp"
"strings"
"github.com/spf13/afero"
)
// ImageInfo retrieves OS image information.
type ImageInfo struct {
fs *afero.Afero
}
// NewImageInfo creates a new imageInfo.
func NewImageInfo() *ImageInfo {
return &ImageInfo{
fs: &afero.Afero{Fs: afero.NewOsFs()},
}
}
// ImageVersion tries to parse the image version from the host mounted os-release file.
// If the file is not present or does not contain the version, a fallback lookup is performed.
func (i *ImageInfo) ImageVersion(imageReference string) (string, error) {
var version string
var err error
for _, path := range osReleasePaths {
version, err = i.getOSReleaseImageVersion(path)
if err == nil {
break
}
}
if version != "" {
return version, nil
}
return imageVersionFromFallback(imageReference)
}
// getOSReleaseImageVersion reads the os-release file and returns the image version (if present).
func (i *ImageInfo) getOSReleaseImageVersion(path string) (string, error) {
osRelease, err := i.fs.Open(path)
if err != nil {
return "", err
}
defer osRelease.Close()
osReleaseMap, err := parseOSRelease(bufio.NewScanner(osRelease))
if err != nil {
return "", err
}
version, ok := osReleaseMap[versionKey]
if !ok {
return "", fmt.Errorf("IMAGE_VERSION not found in %s", path)
}
return version, nil
}
// parseOSRelease parses the os-release file and returns a map of key-value pairs.
// The os-release file is a simple key-value file.
// The format is specified in https://www.freedesktop.org/software/systemd/man/os-release.html.
func parseOSRelease(osRelease *bufio.Scanner) (map[string]string, error) {
osReleaseMap := make(map[string]string)
for osRelease.Scan() {
line := osRelease.Text()
matches := osReleaseLine.FindStringSubmatch(line)
if len(matches) < 6 {
continue
}
key := matches[1]
var value string
// group 3 is the value with double quotes
// group 4 is the value with single quotes
// group 5 is the value without quotes
for i := 3; i < 6; i++ {
if matches[i] != "" {
value = matches[i]
break
}
}
// unescape the following characters: \\, \$, \", \', \`
value = osReleaseUnescape.ReplaceAllString(value, "$1")
osReleaseMap[key] = value
}
if err := osRelease.Err(); err != nil {
return nil, err
}
return osReleaseMap, nil
}
// imageVersionFromFallback tries to guess the image version from the image reference.
// It is a fallback mechanism in case the os-release file is not present or does not contain the version.
// This was the case for older images (< v2.3.0).
func imageVersionFromFallback(imageReference string) (string, error) {
version, ok := fallbackLookup[strings.ToLower(imageReference)]
if !ok {
return "", fmt.Errorf("image version not found in fallback lookup")
}
return version, nil
}
const versionKey = "IMAGE_VERSION"
var (
osReleaseLine = regexp.MustCompile(`^(?P<name>[a-zA-Z0-9_]+)=("(?P<v1>.*)"|'(?P<v2>.*)'|(?P<v3>[^\n"']+))$`)
osReleaseUnescape = regexp.MustCompile(`\\([\\\$\"\'` + "`" + `])`)
osReleasePaths = []string{
"/host/etc/os-release",
"/host/usr/lib/os-release",
}
fallbackLookup = map[string]string{
// AWS
"ami-06b8cbf4837a0a57c": "v2.2.2",
"ami-02e96dc04a9e438cd": "v2.2.2",
"ami-028ead928a9034b2f": "v2.2.2",
"ami-032ac10dd8d8266e3": "v2.2.1",
"ami-032e0d57cc4395088": "v2.2.1",
"ami-053c3e49e19b96bdd": "v2.2.1",
"ami-0e27ebcefc38f648b": "v2.2.0",
"ami-098cd37f66523b7c3": "v2.2.0",
"ami-04a87d302e2509aad": "v2.2.0",
// Azure
"/communitygalleries/constellationcvm-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.2.2": "v2.2.2",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation/images/constellation/versions/2.2.2": "v2.2.2",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation_cvm/images/constellation/versions/2.2.2": "v2.2.2",
"/communitygalleries/constellationcvm-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.2.1": "v2.2.1",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation/images/constellation/versions/2.2.1": "v2.2.1",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation_cvm/images/constellation/versions/2.2.1": "v2.2.1",
"/communitygalleries/constellationcvm-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.2.0": "v2.2.0",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation/images/constellation/versions/2.2.0": "v2.2.0",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation_cvm/images/constellation/versions/2.2.0": "v2.2.0",
"/communitygalleries/constellationcvm-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.1.0": "v2.1.0",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation/images/constellation/versions/2.1.0": "v2.1.0",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation_cvm/images/constellation/versions/2.1.0": "v2.1.0",
"/communitygalleries/constellationcvm-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.0.0": "v2.0.0",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation/images/constellation/versions/2.0.0": "v2.0.0",
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourcegroups/constellation-images/providers/microsoft.compute/galleries/constellation_cvm/images/constellation/versions/2.0.0": "v2.0.0",
// GCP
"projects/constellation-images/global/images/constellation-v2-2-2": "v2.2.2",
"projects/constellation-images/global/images/constellation-v2-2-1": "v2.2.1",
"projects/constellation-images/global/images/constellation-v2-2-0": "v2.2.0",
"projects/constellation-images/global/images/constellation-v2-1-0": "v2.1.0",
"projects/constellation-images/global/images/constellation-v2-0-0": "v2.0.0",
}
)

View File

@ -0,0 +1,229 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package deploy
import (
"bufio"
"os"
"strings"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestImageVersion(t *testing.T) {
testCases := map[string]struct {
imageReference string
createFile [2]string
wantVersion string
wantErr bool
}{
"version found in /etc": {
imageReference: "some-reference",
createFile: [2]string{"/host/etc/os-release", osRelease},
wantVersion: "v2.3.0",
},
"version found in /usr/lib": {
imageReference: "some-reference",
createFile: [2]string{"/host/usr/lib/os-release", osRelease},
wantVersion: "v2.3.0",
},
"version not found": {
imageReference: "some-reference",
wantErr: true,
},
"fallback version found": {
imageReference: "ami-04a87d302e2509aad",
wantVersion: "v2.2.0",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
fs := afero.NewMemMapFs()
if tc.createFile[0] != "" {
err := afero.WriteFile(fs, tc.createFile[0], []byte(tc.createFile[1]), os.ModePerm)
require.NoError(err)
}
imageInfo := &ImageInfo{
fs: &afero.Afero{Fs: fs},
}
version, err := imageInfo.ImageVersion(tc.imageReference)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantVersion, version)
})
}
}
func TestGetOSReleaseImageVersion(t *testing.T) {
testCases := map[string]struct {
path string
wantVersion string
wantErr bool
}{
"version found": {
path: "os-release",
wantVersion: "v2.3.0",
},
"invalid path": {
path: "not/a/real/path",
wantErr: true,
},
"empty file": {
path: "empty",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
fs := afero.NewMemMapFs()
err := afero.WriteFile(fs, "os-release", []byte(osRelease), os.ModePerm)
require.NoError(err)
err = afero.WriteFile(fs, "empty", []byte{}, os.ModePerm)
require.NoError(err)
imageInfo := &ImageInfo{
fs: &afero.Afero{Fs: fs},
}
version, err := imageInfo.getOSReleaseImageVersion(tc.path)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantVersion, version)
})
}
}
func TestParseOSRelease(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
osReleaseMap, err := parseOSRelease(bufio.NewScanner(strings.NewReader(osRelease)))
require.NoError(err)
assert.Equal(wantMap, osReleaseMap)
}
func TestImageVersionFromFallback(t *testing.T) {
testCases := map[string]struct {
imageReference string
wantVersion string
wantErr bool
}{
"AWS reference": {
imageReference: "ami-06b8cbf4837a0a57c",
wantVersion: "v2.2.2",
},
"Azure reference": {
imageReference: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/constellation-images/providers/Microsoft.Compute/galleries/Constellation/images/constellation/versions/2.1.0",
wantVersion: "v2.1.0",
},
"GCP reference": {
imageReference: "projects/constellation-images/global/images/constellation-v2-0-0",
wantVersion: "v2.0.0",
},
"unknown reference": {
imageReference: "unknown",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
version, err := imageVersionFromFallback(tc.imageReference)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantVersion, version)
})
}
}
const osRelease = `
# Some comment
# Some empty lines below
SINGLE_QUOTED_VALUE='WOW! This is a single quoted value!'
DOUBLE_QUOTED_VALUE="WOW! This is a double quoted value!"
ESCAPED_BACKSLASH='This is a string with an escaped backslash: \\'
ESCAPED_DOLLAR='This is a string with an escaped dollar: \$'
ESCAPED_DOUBLE_QUOTE='This is a string with an escaped double quote: \"'
ESCAPED_SINGLE_QUOTE="This is a string with an escaped single quote: \'"
NAME="Fedora Linux"
VERSION="37 (Thirty Seven)"
ID=fedora
PRETTY_NAME="Fedora Linux 37 (Thirty Seven)"
ANSI_COLOR="0;38;2;60;110;180"
VERSION_ID=37
VERSION_CODENAME=""
PLATFORM_ID="platform:f37"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:37"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f37/system-administrators-guide/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=37
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=37
IMAGE_ID="constellation"
IMAGE_VERSION="v2.3.0"
`
var wantMap = map[string]string{
"NAME": `Fedora Linux`,
"VERSION": `37 (Thirty Seven)`,
"ID": `fedora`,
"SINGLE_QUOTED_VALUE": `WOW! This is a single quoted value!`,
"DOUBLE_QUOTED_VALUE": `WOW! This is a double quoted value!`,
"ESCAPED_BACKSLASH": `This is a string with an escaped backslash: \`,
"ESCAPED_DOLLAR": `This is a string with an escaped dollar: $`,
"ESCAPED_DOUBLE_QUOTE": `This is a string with an escaped double quote: "`,
"ESCAPED_SINGLE_QUOTE": `This is a string with an escaped single quote: '`,
"VERSION_ID": `37`,
"VERSION_CODENAME": ``,
"PLATFORM_ID": `platform:f37`,
"PRETTY_NAME": `Fedora Linux 37 (Thirty Seven)`,
"ANSI_COLOR": `0;38;2;60;110;180`,
"LOGO": `fedora-logo-icon`,
"CPE_NAME": `cpe:/o:fedoraproject:fedora:37`,
"DEFAULT_HOSTNAME": `fedora`,
"HOME_URL": `https://fedoraproject.org/`,
"DOCUMENTATION_URL": `https://docs.fedoraproject.org/en-US/fedora/f37/system-administrators-guide/`,
"SUPPORT_URL": `https://ask.fedoraproject.org/`,
"BUG_REPORT_URL": `https://bugzilla.redhat.com/`,
"REDHAT_BUGZILLA_PRODUCT": `Fedora`,
"REDHAT_BUGZILLA_PRODUCT_VERSION": `37`,
"REDHAT_SUPPORT_PRODUCT": `Fedora`,
"REDHAT_SUPPORT_PRODUCT_VERSION": `37`,
"IMAGE_ID": `constellation`,
"IMAGE_VERSION": `v2.3.0`,
}

View File

@ -126,8 +126,8 @@ func main() {
os.Exit(1)
}
defer etcdClient.Close()
if err := deploy.InitialResources(context.Background(), k8sClient, cspClient, os.Getenv(constellationUID)); err != nil {
imageInfo := deploy.NewImageInfo()
if err := deploy.InitialResources(context.Background(), k8sClient, imageInfo, cspClient, os.Getenv(constellationUID)); err != nil {
setupLog.Error(err, "Unable to deploy initial resources")
os.Exit(1)
}