From 0db9ec08ba1e967ac705abd0b5c45567bea38032 Mon Sep 17 00:00:00 2001 From: Sean Eagan Date: Wed, 24 Feb 2021 16:28:01 -0600 Subject: [PATCH] Add jumphost configuration to ssh to VMs This adds a field to the SIP CRD to reference a Secret containing SSH private keys to inject into the jump host container to be used to SSH into the cluster's nodes. These should correspond to whatever SSH authorized keys that will be included in the nodes. These keys are then added to the jumphost container, and an SSH config file is added to the ubuntu user's SSH config which includes these keys along with host entries for each VM, which allows them to be consumed by bash completion, which this also adds to the jumphost image. Signed-off-by: Sean Eagan Change-Id: If2e948f567a867d8ee11353d79f3224faeac9215 --- .../airship.airshipit.org_sipclusters.yaml | 10 ++ config/rbac/role.yaml | 6 + .../samples/airship_v1beta1_sipcluster.yaml | 3 +- config/samples/kustomization.yaml | 1 + config/samples/ssh_private_keys_secret.yaml | 7 + docs/api/sipcluster.md | 14 ++ go.sum | 10 -- images/jump-host/Dockerfile | 16 +- pkg/api/v1/sipcluster_types.go | 5 + pkg/controllers/sipcluster_controller.go | 1 + pkg/controllers/sipcluster_controller_test.go | 23 ++- pkg/controllers/suite_test.go | 4 + pkg/services/jumphost.go | 153 ++++++++++++++---- pkg/services/services_test.go | 23 ++- pkg/vbmh/vbmh_test.go | 27 ++-- testutil/testutil.go | 99 +++++++----- 16 files changed, 291 insertions(+), 111 deletions(-) create mode 100644 config/samples/ssh_private_keys_secret.yaml diff --git a/config/crd/bases/airship.airshipit.org_sipclusters.yaml b/config/crd/bases/airship.airshipit.org_sipclusters.yaml index bd1c82d..4d4d5e5 100644 --- a/config/crd/bases/airship.airshipit.org_sipclusters.yaml +++ b/config/crd/bases/airship.airshipit.org_sipclusters.yaml @@ -120,6 +120,15 @@ spec: type: object nodePort: type: integer + nodeSSHPrivateKeys: + description: NodeSSHPrivateKeys holds the name of a Secret + in the same namespace as the SIPCluster CR, whose key values + each represent an ssh private key that can be used to access + the cluster nodes. They are mounted into the jumphost with + the secret keys serving as file names relative to a common + directory, and then configured as identity files in the + SSH config file of the default user. + type: string sshAuthorizedKeys: items: type: string @@ -127,6 +136,7 @@ spec: required: - image - nodePort + - nodeSSHPrivateKeys type: object type: array loadBalancer: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c899040..310f120 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -6,6 +6,12 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get - apiGroups: - airship.airshipit.org resources: diff --git a/config/samples/airship_v1beta1_sipcluster.yaml b/config/samples/airship_v1beta1_sipcluster.yaml index 01327b2..d44f34a 100644 --- a/config/samples/airship_v1beta1_sipcluster.yaml +++ b/config/samples/airship_v1beta1_sipcluster.yaml @@ -1,7 +1,7 @@ apiVersion: airship.airshipit.org/v1 kind: SIPCluster metadata: - name: sipcluster-test + name: sipcluster-system namespace: sipcluster-system finalizers: - sip.airship.airshipit.org/finalizer @@ -43,6 +43,7 @@ spec: sshAuthorizedKeys: - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCyaozS8kZRw2a1d0O4YXhxtJlDPThqIZilGCsXLbukIFOyMUmMTwQAtwWp5epwU1+5ponC2uBENB6xCCj3cl5Rd43d2/B6HxyAPQGKo6/zKYGAKW2nzYDxSWMl6NUSsiJAyXUA7ZlNZQe0m8PmaferlkQyLLZo3NJpizz6U6ZCtxvj43vEl7NYWnLUEIzGP9zMqltIGnD4vYrU9keVKKXSsp+DkApnbrDapeigeGATCammy2xRrUQDuOvGHsfnQbXr2j0onpTIh0PiLrXLQAPDg8UJRgVB+ThX+neI3rQ320djzRABckNeE6e4Kkwzn+QdZsmA2SDvM9IU7boK1jVQlgUPp7zF5q3hbb8Rx7AadyTarBayUkCgNlrMqth+tmTMWttMqCPxJRGnhhvesAHIl55a28Kzz/2Oqa3J9zwzbyDIwlEXho0eAq3YXEPeBhl34k+7gOt/5Zdbh+yacFoxDh0LrshQgboAijcVVaXPeN0LsHEiVvYIzugwIvCkoFMPWoPj/kEGzPY6FCkVneDA7VoLTCoG8dlrN08Lf05/BGC7Wllm66pTNZC/cKXP+cjpQn1iEuiuPxnPldlMHx9sx2y/BRoft6oT/GzqkNy1NTY/xI+MfmxXnF5kwSbcTbzZQ9fZ8xjh/vmpPBgDNrxOEAT4N6OG7GQIhb9HEhXQCQ== example-key - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwpOyZjZ4gB0OTvmofH3llh6cBCWaEiEmHZWSkDXr8Bih6HcXVOtYMcFi/ZnUVGUBPw3ATNQBZUaVCYKeF+nDfKTJ9hmnlsyHxV2LeMsVg1o15Pb6f+QJuavEqtE6HI7mHyId4Z1quVTJXDWDW8OZEG7M3VktauqAn/e9UJvlL0bGmTFD1XkNcbRsWMRWkQgt2ozqlgrpPtvrg2/+bNucxX++VUjnsn+fGgAT07kbnrZwppGnAfjbYthxhv7GeSD0+Z0Lf1kiKy/bhUqXsZIuexOfF0YrRyUH1KBl8GCX2OLBYvXHyusByqsrOPiROqRdjX5PsK6HSAS0lk0niTt1p example-key-2 + nodeSSHPrivateKeys: ssh-private-keys loadBalancer: - image: haproxy:2.3.2 # NOTE: nodeLabels not yet implemented. diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index f54db91..0576add 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,5 @@ resources: - airship_v1beta1_sipcluster.yaml - bmh +- ssh_private_keys_secret.yaml namespace: sipcluster-system diff --git a/config/samples/ssh_private_keys_secret.yaml b/config/samples/ssh_private_keys_secret.yaml new file mode 100644 index 0000000..da3c5df --- /dev/null +++ b/config/samples/ssh_private_keys_secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + key: RFVNTVlfREFUQQ== +kind: Secret +metadata: + name: ssh-private-keys +type: Opaque \ No newline at end of file diff --git a/docs/api/sipcluster.md b/docs/api/sipcluster.md index 436adfc..018e46a 100644 --- a/docs/api/sipcluster.md +++ b/docs/api/sipcluster.md @@ -94,6 +94,20 @@ BMCOpts + + +nodeSSHPrivateKeys
+ +string + + + +

NodeSSHPrivateKeys holds the name of a Secret in the same namespace as the SIPCluster CR, +whose key values each represent an ssh private key that can be used to access the cluster nodes. +They are mounted into the jumphost with the secret keys serving as file names relative to a common +directory, and then configured as identity files in the SSH config file of the default user.

+ + diff --git a/go.sum b/go.sum index 9d76ee4..d9a2c62 100644 --- a/go.sum +++ b/go.sum @@ -822,25 +822,19 @@ k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlm k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc= k8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg= -k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg= -k8s.io/apiserver v0.19.2 h1:xq2dXAzsAoHv7S4Xc/p7PKhiowdHV/PgdePWo3MxIYM= k8s.io/apiserver v0.19.2/go.mod h1:FreAq0bJ2vtZFj9Ago/X0oNGC51GfubKK/ViOKfVAOA= k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q= k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= k8s.io/client-go v0.19.2 h1:gMJuU3xJZs86L1oQ99R4EViAADUPMHHtS9jFshasHSc= k8s.io/client-go v0.19.2/go.mod h1:S5wPhCqyDNAlzM9CnEdgTGV4OqhsW3jGO1UM1epwfJA= k8s.io/code-generator v0.18.6/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= -k8s.io/code-generator v0.18.6/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= -k8s.io/code-generator v0.19.2 h1:7uaWJll6fyCPj2j3sfNN1AiY2gZU1VFN2dFR2uoxGWI= k8s.io/code-generator v0.19.2/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= k8s.io/component-base v0.18.6/go.mod h1:knSVsibPR5K6EW2XOjEHik6sdU5nCvKMrzMt2D4In14= -k8s.io/component-base v0.18.6/go.mod h1:knSVsibPR5K6EW2XOjEHik6sdU5nCvKMrzMt2D4In14= k8s.io/component-base v0.19.2 h1:jW5Y9RcZTb79liEhW3XDVTW7MuvEGP0tQZnfSX6/+gs= k8s.io/component-base v0.19.2/go.mod h1:g5LrsiTiabMLZ40AR6Hl45f088DevyGY+cCE2agEIVo= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14 h1:t4L10Qfx/p7ASH3gXCdIUtPbbIuegCoUJf3TMSFekjw= k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -858,17 +852,13 @@ k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20200821003339-5e75c0163111/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20200912215256-4140de9c8800 h1:9ZNvfPvVIEsp/T1ez4GQuzCcCTEQWhovSofhqR73A6g= k8s.io/utils v0.0.0-20200912215256-4140de9c8800/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9 h1:rusRLrDhjBp6aYtl9sGEvQJr6faoHoDLd0YcUBTZguI= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9/go.mod h1:dzAXnQbTRyDlZPJX2SUPEqvnB+j7AJjtlox7PEwigU0= sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= -sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= sigs.k8s.io/controller-runtime v0.7.0 h1:bU20IBBEPccWz5+zXpLnpVsgBYxqclaHu1pVDl/gEt8= sigs.k8s.io/controller-runtime v0.7.0/go.mod h1:pJ3YBrJiAqMAZKi6UVGuE98ZrroV1p+pIhoHsMm9wdU= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= diff --git a/images/jump-host/Dockerfile b/images/jump-host/Dockerfile index 1dff3ed..7b1d4fe 100644 --- a/images/jump-host/Dockerfile +++ b/images/jump-host/Dockerfile @@ -10,7 +10,21 @@ COPY ./certs/* /usr/local/share/ca-certificates/ RUN update-ca-certificates RUN apt-get update -RUN apt-get install -y --no-install-recommends jq openssh-server python3-pip python3-setuptools +RUN apt-get install -y --no-install-recommends \ + bash-completion \ + jq \ + python3-pip \ + python3-setuptools \ + openssh-server \ + openssh-client + +# uncomment (enable) bash completion config +RUN START=$(sed -n '/# enable bash completion in interactive shells/=' /etc/bash.bashrc) && \ + sed -i "$((START + 1)),$((START + 7))"' s/^##*//' /etc/bash.bashrc +# disable bash completion based on /etc/hosts, /etc/known_hosts, etc. +# so that only ssh config file entries are used +ENV COMP_KNOWN_HOSTS_WITH_HOSTFILE= + RUN pip3 install --upgrade pip RUN pip3 config set global.cert /etc/ssl/certs/ca-certificates.crt diff --git a/pkg/api/v1/sipcluster_types.go b/pkg/api/v1/sipcluster_types.go index b04c419..f87a543 100644 --- a/pkg/api/v1/sipcluster_types.go +++ b/pkg/api/v1/sipcluster_types.go @@ -85,6 +85,11 @@ type JumpHostService struct { SIPClusterService `json:",inline"` BMC *BMCOpts `json:"bmc,omitempty"` SSHAuthorizedKeys []string `json:"sshAuthorizedKeys,omitempty"` + // NodeSSHPrivateKeys holds the name of a Secret in the same namespace as the SIPCluster CR, + // whose key values each represent an ssh private key that can be used to access the cluster nodes. + // They are mounted into the jumphost with the secret keys serving as file names relative to a common + // directory, and then configured as identity files in the SSH config file of the default user. + NodeSSHPrivateKeys string `json:"nodeSSHPrivateKeys"` } // SIPClusterStatus defines the observed state of SIPCluster diff --git a/pkg/controllers/sipcluster_controller.go b/pkg/controllers/sipcluster_controller.go index 72fa072..faf70f3 100644 --- a/pkg/controllers/sipcluster_controller.go +++ b/pkg/controllers/sipcluster_controller.go @@ -51,6 +51,7 @@ const ( // +kubebuilder:rbac:groups=airship.airshipit.org,resources=sipclusters/status,verbs=get;update;patch // +kubebuilder:rbac:groups="metal3.io",resources=baremetalhosts,verbs=get;update;patch;list +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get; func (r *SIPClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.NamespacedName = req.NamespacedName diff --git a/pkg/controllers/sipcluster_controller_test.go b/pkg/controllers/sipcluster_controller_test.go index 480a00a..c065c57 100644 --- a/pkg/controllers/sipcluster_controller_test.go +++ b/pkg/controllers/sipcluster_controller_test.go @@ -41,7 +41,7 @@ const ( var _ = Describe("SIPCluster controller", func() { AfterEach(func() { - opts := []client.DeleteAllOfOption{client.InNamespace("default")} + opts := []client.DeleteAllOfOption{client.InNamespace(testNamespace)} Expect(k8sClient.DeleteAllOf(context.Background(), &metal3.BareMetalHost{}, opts...)).Should(Succeed()) Expect(k8sClient.DeleteAllOf(context.Background(), &airshipv1.SIPCluster{}, opts...)).Should(Succeed()) Expect(k8sClient.DeleteAllOf(context.Background(), &corev1.Secret{}, opts...)).Should(Succeed()) @@ -71,7 +71,8 @@ var _ = Describe("SIPCluster controller", func() { // Create SIP cluster name := "subcluster-test1" - sipCluster := testutil.CreateSIPCluster(name, testNamespace, 3, 4) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster(name, testNamespace, 3, 4) + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), sipCluster)).Should(Succeed()) // Poll BMHs until SIP has scheduled them to the SIP cluster @@ -107,7 +108,8 @@ var _ = Describe("SIPCluster controller", func() { // Create SIP cluster name := "subcluster-test2" - sipCluster := testutil.CreateSIPCluster(name, testNamespace, 3, 4) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster(name, testNamespace, 3, 4) + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), sipCluster)).Should(Succeed()) // Poll BMHs and validate they are not scheduled @@ -153,7 +155,8 @@ var _ = Describe("SIPCluster controller", func() { // Create SIP cluster name := "subcluster-test4" - sipCluster := testutil.CreateSIPCluster(name, testNamespace, 3, 4) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster(name, testNamespace, 3, 4) + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), sipCluster)).Should(Succeed()) // Poll BMHs and validate they are not scheduled @@ -215,7 +218,8 @@ var _ = Describe("SIPCluster controller", func() { // Create SIP cluster name := "subcluster-test5" - sipCluster := testutil.CreateSIPCluster(name, testNamespace, 1, 2) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster(name, testNamespace, 1, 2) + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), sipCluster)).Should(Succeed()) // Poll BMHs and validate they are not scheduled @@ -276,7 +280,8 @@ var _ = Describe("SIPCluster controller", func() { // Create SIP cluster name := "subcluster-test6" - sipCluster := testutil.CreateSIPCluster(name, testNamespace, 2, 1) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster(name, testNamespace, 2, 1) + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), sipCluster)).Should(Succeed()) // Poll BMHs and validate they are not scheduled @@ -336,7 +341,7 @@ var _ = Describe("SIPCluster controller", func() { // Create SIP cluster name := "subcluster-test3" - sipCluster := testutil.CreateSIPCluster(name, testNamespace, 1, 2) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster(name, testNamespace, 1, 2) controlPlaneSpec := sipCluster.Spec.Nodes[airshipv1.VMControlPlane] controlPlaneSpec.Scheduling = airshipv1.RackAntiAffinity @@ -346,6 +351,7 @@ var _ = Describe("SIPCluster controller", func() { workerSpec.Scheduling = airshipv1.RackAntiAffinity sipCluster.Spec.Nodes[airshipv1.VMWorker] = workerSpec + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), sipCluster)).Should(Succeed()) // Poll BMHs and validate they are not scheduled @@ -402,7 +408,7 @@ var _ = Describe("SIPCluster controller", func() { // Create SIP cluster name := "subcluster-test3" - sipCluster := testutil.CreateSIPCluster(name, testNamespace, 2, 1) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster(name, testNamespace, 2, 1) controlPlaneSpec := sipCluster.Spec.Nodes[airshipv1.VMControlPlane] controlPlaneSpec.Scheduling = airshipv1.RackAntiAffinity @@ -412,6 +418,7 @@ var _ = Describe("SIPCluster controller", func() { workerSpec.Scheduling = airshipv1.RackAntiAffinity sipCluster.Spec.Nodes[airshipv1.VMWorker] = workerSpec + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) Expect(k8sClient.Create(context.Background(), sipCluster)).Should(Succeed()) // Poll BMHs and validate they are not scheduled diff --git a/pkg/controllers/suite_test.go b/pkg/controllers/suite_test.go index a0120f8..7e3ff8d 100644 --- a/pkg/controllers/suite_test.go +++ b/pkg/controllers/suite_test.go @@ -25,6 +25,7 @@ import ( metal3 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -72,6 +73,9 @@ var _ = BeforeSuite(func(done Done) { err = metal3.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = corev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ diff --git a/pkg/services/jumphost.go b/pkg/services/jumphost.go index 318f4ee..a39c0e5 100644 --- a/pkg/services/jumphost.go +++ b/pkg/services/jumphost.go @@ -15,8 +15,11 @@ package services import ( + "bytes" + "context" "encoding/json" "fmt" + "html/template" "net/url" "strings" @@ -35,16 +38,23 @@ import ( const ( JumpHostServiceName = "jumphost" - mountPathData = "/etc/opt/sip" - mountPathScripts = "/opt/sip/bin" + subPathHosts = "hosts" + subPathSSHConfig = "ssh_config" sshDir = "/home/ubuntu/.ssh" authorizedKeysFile = "authorized_keys" - mountPathSSH = sshDir + "/" + authorizedKeysFile - nameAuthorizedKeysVolume = "authorized-keys" - nameHostsVolume = "hosts" - nameRebootVolume = "vm" + mountPathData = "/etc/opt/sip" + mountPathScripts = "/opt/sip/bin" + mountPathHosts = mountPathData + "/" + subPathHosts + mountPathSSHConfig = sshDir + "/config" + mountPathSSH = sshDir + "/" + authorizedKeysFile + mountPathNodeSSHPrivateKeys = mountPathData + "/" + nameNodeSSHPrivateKeysVolume + + nameDataVolume = "data" + nameScriptsVolume = "scripts" + nameAuthorizedKeysVolume = "authorized-keys" + nameNodeSSHPrivateKeysVolume = "ssh-private-keys" ) // JumpHost is an InfrastructureService that provides SSH and power-management capabilities for sub-clusters. @@ -81,6 +91,8 @@ func (jh jumpHost) Deploy() error { "app.kubernetes.io/instance": instance, } + hostAliases := jh.generateHostAliases() + // TODO: Validate Service becomes ready. service := jh.generateService(instance, labels) jh.logger.Info("Applying service", "service", service.GetNamespace()+"/"+service.GetName()) @@ -90,8 +102,7 @@ func (jh jumpHost) Deploy() error { return err } - // TODO: Validate Secret becomes ready. - secret, err := jh.generateSecret(instance, labels) + secret, err := jh.generateSecret(instance, labels, hostAliases) if err != nil { return err } @@ -115,7 +126,7 @@ func (jh jumpHost) Deploy() error { } // TODO: Validate Deployment becomes ready. - deployment := jh.generateDeployment(instance, labels) + deployment := jh.generateDeployment(instance, labels, hostAliases) jh.logger.Info("Applying deployment", "deployment", deployment.GetNamespace()+"/"+deployment.GetName()) err = applyRuntimeObject(client.ObjectKey{Name: deployment.GetName(), Namespace: deployment.GetNamespace()}, deployment, jh.client) @@ -126,7 +137,8 @@ func (jh jumpHost) Deploy() error { return nil } -func (jh jumpHost) generateDeployment(instance string, labels map[string]string) *appsv1.Deployment { +func (jh jumpHost) generateDeployment(instance string, labels map[string]string, + hostAliases []corev1.HostAlias) *appsv1.Deployment { deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: instance, @@ -166,19 +178,29 @@ func (jh jumpHost) generateDeployment(instance string, labels map[string]string) }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: nameDataVolume, + MountPath: mountPathHosts, + SubPath: subPathHosts, + }, + { + Name: nameScriptsVolume, + MountPath: mountPathScripts, + }, + { + Name: nameDataVolume, + MountPath: mountPathSSHConfig, + SubPath: subPathSSHConfig, + }, + { + Name: nameNodeSSHPrivateKeysVolume, + MountPath: mountPathNodeSSHPrivateKeys, + }, { Name: nameAuthorizedKeysVolume, MountPath: mountPathSSH, SubPath: authorizedKeysFile, }, - { - Name: nameHostsVolume, - MountPath: mountPathData, - }, - { - Name: nameRebootVolume, - MountPath: mountPathScripts, - }, }, }, }, @@ -187,7 +209,7 @@ func (jh jumpHost) generateDeployment(instance string, labels map[string]string) }, Volumes: []corev1.Volume{ { - Name: nameHostsVolume, + Name: nameDataVolume, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: instance, @@ -213,24 +235,26 @@ func (jh jumpHost) generateDeployment(instance string, labels map[string]string) }, }, { - Name: nameRebootVolume, + Name: nameScriptsVolume, VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ Name: instance, }, DefaultMode: int32Ptr(0777), - Items: []corev1.KeyToPath{ - { - Key: nameRebootVolume, - Path: nameRebootVolume, - }, - }, + }, + }, + }, + { + Name: nameNodeSSHPrivateKeysVolume, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: jh.config.NodeSSHPrivateKeys, }, }, }, }, - HostAliases: jh.generateHostAliases(), + HostAliases: hostAliases, }, }, }, @@ -281,18 +305,23 @@ func (jh jumpHost) generateConfigMap(instance string, labels map[string]string) }, Data: map[string]string{ nameAuthorizedKeysVolume: strings.Join(jh.config.SSHAuthorizedKeys, "\n"), - nameRebootVolume: fmt.Sprintf(rebootScript, mountPathData, nameHostsVolume), + "vm": fmt.Sprintf(rebootScript, mountPathHosts), }, }, nil } -func (jh jumpHost) generateSecret(instance string, labels map[string]string) (*corev1.Secret, error) { +func (jh jumpHost) generateSecret(instance string, labels map[string]string, hostAliases []corev1.HostAlias) ( + *corev1.Secret, error) { hostData, err := generateHostList(*jh.machines) if err != nil { return nil, err } + sshConfig, err := jh.generateSSHConfig(hostAliases) + if err != nil { + return nil, err + } - return &corev1.Secret{ + secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", @@ -303,11 +332,69 @@ func (jh jumpHost) generateSecret(instance string, labels map[string]string) (*c Labels: labels, }, Data: map[string][]byte{ - nameHostsVolume: hostData, + subPathHosts: hostData, + subPathSSHConfig: sshConfig, }, - }, nil + } + + return secret, nil } +func (jh jumpHost) generateSSHConfig(hostAliases []corev1.HostAlias) ([]byte, error) { + key := types.NamespacedName{ + Namespace: jh.sipName.Namespace, + Name: jh.config.NodeSSHPrivateKeys, + } + secret := &corev1.Secret{} + if err := jh.client.Get(context.Background(), key, secret); err != nil { + return nil, err + } + + identityFiles := []string{} + for k := range secret.Data { + identityFiles = append(identityFiles, mountPathNodeSSHPrivateKeys+"/"+k) + } + hostNames := []string{} + for _, hostAlias := range hostAliases { + hostNames = append(hostNames, hostAlias.Hostnames[0]) + } + + tmpl, err := template.New("ssh-config").Parse(sshConfigTemplate) + if err != nil { + return nil, err + } + + data := sshConfigTemplateData{ + IdentityFiles: identityFiles, + HostNames: hostNames, + } + + w := bytes.NewBuffer([]byte{}) + if err := tmpl.Execute(w, data); err != nil { + return nil, err + } + + rendered := w.Bytes() + return rendered, nil +} + +type sshConfigTemplateData struct { + IdentityFiles []string + HostNames []string +} + +const sshConfigTemplate = ` +Host * +{{- range .IdentityFiles }} + IdentityFile {{ . }} +{{ end -}} + +{{- range .HostNames }} +Host {{ . }} + HostName {{ . }} +{{ end -}} +` + func (jh jumpHost) generateHostAliases() []corev1.HostAlias { hostAliases := []corev1.HostAlias{} for _, machine := range jh.machines.Machines { @@ -410,7 +497,7 @@ var rebootScript = `#!/bin/sh # Support Infrastructure Provider (SIP) VM Utility # DO NOT MODIFY: generated by SIP -HOSTS_FILE="%s/%s" +HOSTS_FILE="%s" LIST_COMMAND="list" REBOOT_COMMAND="reboot" diff --git a/pkg/services/services_test.go b/pkg/services/services_test.go index da9b6a1..2df11ac 100644 --- a/pkg/services/services_test.go +++ b/pkg/services/services_test.go @@ -27,6 +27,9 @@ const ( var bmh1 *metal3.BareMetalHost var bmh2 *metal3.BareMetalHost +var m1 *vbmh.Machine +var m2 *vbmh.Machine + // Re-declared from services package for testing purposes type host struct { Name string `json:"name"` @@ -54,7 +57,7 @@ var _ = Describe("Service Set", func() { bmh1.Spec.BMC.CredentialsName = bmcSecret.Name bmh2.Spec.BMC.CredentialsName = bmcSecret.Name - m1 := &vbmh.Machine{ + m1 = &vbmh.Machine{ BMH: *bmh1, Data: &vbmh.MachineData{ IPOnInterface: map[string]string{ @@ -63,7 +66,7 @@ var _ = Describe("Service Set", func() { }, } - m2 := &vbmh.Machine{ + m2 = &vbmh.Machine{ BMH: *bmh2, Data: &vbmh.MachineData{ IPOnInterface: map[string]string{ @@ -91,8 +94,16 @@ var _ = Describe("Service Set", func() { It("Deploys services", func() { By("Getting machine IPs and creating secrets, pods, and nodeport service") - sip := testutil.CreateSIPCluster("default", "default", 1, 1) - set := services.NewServiceSet(logger, *sip, machineList, k8sClient) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("default", "default", 1, 1) + Expect(k8sClient.Create(context.Background(), nodeSSHPrivateKeys)).Should(Succeed()) + machineList = &vbmh.MachineList{ + Machines: map[string]*vbmh.Machine{ + bmh1.GetName(): m1, + bmh2.GetName(): m2, + }, + } + + set := services.NewServiceSet(logger, *sipCluster, machineList, k8sClient) serviceList, err := set.ServiceList() Expect(serviceList).To(HaveLen(2)) @@ -103,12 +114,12 @@ var _ = Describe("Service Set", func() { } Eventually(func() error { - return testDeployment(sip, *machineList) + return testDeployment(sipCluster, *machineList) }, 5, 1).Should(Succeed()) }) It("Does not deploy a Jump Host when an invalid SSH key is provided", func() { - sip := testutil.CreateSIPCluster("default", "default", 1, 1) + sip, _ := testutil.CreateSIPCluster("default", "default", 1, 1) sip.Spec.Services.Auth = []airshipv1.SIPClusterService{} sip.Spec.Services.LoadBalancer = []airshipv1.SIPClusterService{} sip.Spec.Services.JumpHost[0].SSHAuthorizedKeys = []string{ diff --git a/pkg/vbmh/vbmh_test.go b/pkg/vbmh/vbmh_test.go index a856d1f..af27f6e 100644 --- a/pkg/vbmh/vbmh_test.go +++ b/pkg/vbmh/vbmh_test.go @@ -124,7 +124,7 @@ var _ = Describe("MachineList", func() { Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), } - sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) sipCluster.Spec.Services = airshipv1.SIPClusterServices{ LoadBalancer: []airshipv1.SIPClusterService{ { @@ -137,6 +137,7 @@ var _ = Describe("MachineList", func() { }, }, } + objsToApply = append(objsToApply, nodeSSHPrivateKeys) k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.ExtrapolateServiceAddresses(*sipCluster, k8sClient)).To(BeNil()) @@ -174,7 +175,7 @@ var _ = Describe("MachineList", func() { Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), } - sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) sipCluster.Spec.Services = airshipv1.SIPClusterServices{ LoadBalancer: []airshipv1.SIPClusterService{ { @@ -187,6 +188,7 @@ var _ = Describe("MachineList", func() { }, }, } + objsToApply = append(objsToApply, nodeSSHPrivateKeys) k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.ExtrapolateBMCAuth(*sipCluster, k8sClient)).To(BeNil()) @@ -222,7 +224,7 @@ var _ = Describe("MachineList", func() { Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), } - sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) sipCluster.Spec.Services = airshipv1.SIPClusterServices{ LoadBalancer: []airshipv1.SIPClusterService{ { @@ -235,6 +237,7 @@ var _ = Describe("MachineList", func() { }, }, } + objsToApply = append(objsToApply, nodeSSHPrivateKeys) k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.ExtrapolateBMCAuth(*sipCluster, k8sClient)).ToNot(BeNil()) }) @@ -274,7 +277,7 @@ var _ = Describe("MachineList", func() { Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), } - sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) sipCluster.Spec.Services = airshipv1.SIPClusterServices{ LoadBalancer: []airshipv1.SIPClusterService{ { @@ -287,6 +290,7 @@ var _ = Describe("MachineList", func() { }, }, } + objsToApply = append(objsToApply, nodeSSHPrivateKeys) k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.ExtrapolateBMCAuth(*sipCluster, k8sClient)).ToNot(BeNil()) }) @@ -320,7 +324,7 @@ var _ = Describe("MachineList", func() { Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), } - sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) sipCluster.Spec.Services = airshipv1.SIPClusterServices{ LoadBalancer: []airshipv1.SIPClusterService{ { @@ -333,6 +337,7 @@ var _ = Describe("MachineList", func() { }, }, } + objsToApply = append(objsToApply, nodeSSHPrivateKeys) k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.ExtrapolateServiceAddresses(*sipCluster, k8sClient)).ToNot(BeNil()) }) @@ -365,7 +370,7 @@ var _ = Describe("MachineList", func() { Log: ctrl.Log.WithName("controllers").WithName("SIPCluster"), } - sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) sipCluster.Spec.Services = airshipv1.SIPClusterServices{ LoadBalancer: []airshipv1.SIPClusterService{ { @@ -378,22 +383,24 @@ var _ = Describe("MachineList", func() { }, }, } + objsToApply = append(objsToApply, nodeSSHPrivateKeys) k8sClient := mockClient.NewFakeClient(objsToApply...) Expect(ml.ExtrapolateServiceAddresses(*sipCluster, k8sClient)).ToNot(BeNil()) }) It("Should not retrieve the BMH IP if it has been previously extrapolated", func() { // Store an IP address for each machine - var objs []runtime.Object + var objectsToApply []runtime.Object for _, machine := range machineList.Machines { machine.Data.IPOnInterface = map[string]string{ "oam-ipv4": "32.68.51.139", } - objs = append(objs, &machine.BMH) + objectsToApply = append(objectsToApply, &machine.BMH) } - k8sClient := mockClient.NewFakeClient(objs...) - sipCluster := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + sipCluster, nodeSSHPrivateKeys := testutil.CreateSIPCluster("subcluster-1", "default", 1, 3) + objectsToApply = append(objectsToApply, nodeSSHPrivateKeys) + k8sClient := mockClient.NewFakeClient(objectsToApply...) Expect(machineList.ExtrapolateServiceAddresses(*sipCluster, k8sClient)).To(BeNil()) }) diff --git a/testutil/testutil.go b/testutil/testutil.go index 6443fc0..5559aa2 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -23,6 +23,8 @@ const ( VinoFlavorLabel = "vino.airshipit.org/flavor" + sshPrivateKeyBase64 = "DUMMY_DATA" + networkDataContent = ` { "links": [ @@ -207,59 +209,72 @@ func CreateBMH(node int, namespace string, role airshipv1.VMRole, rack int) (*me } // CreateSIPCluster initializes a SIPCluster with specific parameters for use in test cases. -func CreateSIPCluster(name string, namespace string, controlPlanes int, workers int) *airshipv1.SIPCluster { +func CreateSIPCluster(name string, namespace string, controlPlanes int, workers int) ( + *airshipv1.SIPCluster, *corev1.Secret) { + sshPrivateKeySecretName := fmt.Sprintf("%s-ssh-private-key", name) return &airshipv1.SIPCluster{ - TypeMeta: metav1.TypeMeta{ - Kind: "SIPCluster", - APIVersion: "airship.airshipit.org/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: airshipv1.SIPClusterSpec{ - Nodes: map[airshipv1.VMRole]airshipv1.NodeSet{ - airshipv1.VMControlPlane: { - VMFlavor: "vino.airshipit.org/flavor=" + vinoFlavorMap[airshipv1.VMControlPlane], - Scheduling: airshipv1.HostAntiAffinity, - Count: &airshipv1.VMCount{ - Active: controlPlanes, - Standby: 0, - }, - }, - airshipv1.VMWorker: { - VMFlavor: "vino.airshipit.org/flavor=" + vinoFlavorMap[airshipv1.VMWorker], - Scheduling: airshipv1.HostAntiAffinity, - Count: &airshipv1.VMCount{ - Active: workers, - Standby: 0, - }, - }, + TypeMeta: metav1.TypeMeta{ + Kind: "SIPCluster", + APIVersion: "airship.airshipit.org/v1", }, - Services: airshipv1.SIPClusterServices{ - LoadBalancer: []airshipv1.SIPClusterService{ - { - NodeInterface: "eno3", - NodePort: 30000, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: airshipv1.SIPClusterSpec{ + Nodes: map[airshipv1.VMRole]airshipv1.NodeSet{ + airshipv1.VMControlPlane: { + VMFlavor: "vino.airshipit.org/flavor=" + vinoFlavorMap[airshipv1.VMControlPlane], + Scheduling: airshipv1.HostAntiAffinity, + Count: &airshipv1.VMCount{ + Active: controlPlanes, + Standby: 0, + }, + }, + airshipv1.VMWorker: { + VMFlavor: "vino.airshipit.org/flavor=" + vinoFlavorMap[airshipv1.VMWorker], + Scheduling: airshipv1.HostAntiAffinity, + Count: &airshipv1.VMCount{ + Active: workers, + Standby: 0, + }, }, }, - JumpHost: []airshipv1.JumpHostService{ - { - SIPClusterService: airshipv1.SIPClusterService{ - Image: "quay.io/airshipit/jump-host", - NodePort: 30001, + Services: airshipv1.SIPClusterServices{ + LoadBalancer: []airshipv1.SIPClusterService{ + { NodeInterface: "eno3", + NodePort: 30000, }, - SSHAuthorizedKeys: []string{ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCyaozS8kZRw2a1d0O4YXhxtJlDPThqIZilGCsXLbukIFOyMUmMTwQAtwWp5epwU1+5ponC2uBENB6xCCj3cl5Rd43d2/B6HxyAPQGKo6/zKYGAKW2nzYDxSWMl6NUSsiJAyXUA7ZlNZQe0m8PmaferlkQyLLZo3NJpizz6U6ZCtxvj43vEl7NYWnLUEIzGP9zMqltIGnD4vYrU9keVKKXSsp+DkApnbrDapeigeGATCammy2xRrUQDuOvGHsfnQbXr2j0onpTIh0PiLrXLQAPDg8UJRgVB+ThX+neI3rQ320djzRABckNeE6e4Kkwzn+QdZsmA2SDvM9IU7boK1jVQlgUPp7zF5q3hbb8Rx7AadyTarBayUkCgNlrMqth+tmTMWttMqCPxJRGnhhvesAHIl55a28Kzz/2Oqa3J9zwzbyDIwlEXho0eAq3YXEPeBhl34k+7gOt/5Zdbh+yacFoxDh0LrshQgboAijcVVaXPeN0LsHEiVvYIzugwIvCkoFMPWoPj/kEGzPY6FCkVneDA7VoLTCoG8dlrN08Lf05/BGC7Wllm66pTNZC/cKXP+cjpQn1iEuiuPxnPldlMHx9sx2y/BRoft6oT/GzqkNy1NTY/xI+MfmxXnF5kwSbcTbzZQ9fZ8xjh/vmpPBgDNrxOEAT4N6OG7GQIhb9HEhXQCQ== example-key", //nolint - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwpOyZjZ4gB0OTvmofH3llh6cBCWaEiEmHZWSkDXr8Bih6HcXVOtYMcFi/ZnUVGUBPw3ATNQBZUaVCYKeF+nDfKTJ9hmnlsyHxV2LeMsVg1o15Pb6f+QJuavEqtE6HI7mHyId4Z1quVTJXDWDW8OZEG7M3VktauqAn/e9UJvlL0bGmTFD1XkNcbRsWMRWkQgt2ozqlgrpPtvrg2/+bNucxX++VUjnsn+fGgAT07kbnrZwppGnAfjbYthxhv7GeSD0+Z0Lf1kiKy/bhUqXsZIuexOfF0YrRyUH1KBl8GCX2OLBYvXHyusByqsrOPiROqRdjX5PsK6HSAS0lk0niTt1p example-key-2", // nolint + }, + JumpHost: []airshipv1.JumpHostService{ + { + SIPClusterService: airshipv1.SIPClusterService{ + Image: "quay.io/airshipit/jump-host", + NodePort: 30001, + NodeInterface: "eno3", + }, + SSHAuthorizedKeys: []string{ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCyaozS8kZRw2a1d0O4YXhxtJlDPThqIZilGCsXLbukIFOyMUmMTwQAtwWp5epwU1+5ponC2uBENB6xCCj3cl5Rd43d2/B6HxyAPQGKo6/zKYGAKW2nzYDxSWMl6NUSsiJAyXUA7ZlNZQe0m8PmaferlkQyLLZo3NJpizz6U6ZCtxvj43vEl7NYWnLUEIzGP9zMqltIGnD4vYrU9keVKKXSsp+DkApnbrDapeigeGATCammy2xRrUQDuOvGHsfnQbXr2j0onpTIh0PiLrXLQAPDg8UJRgVB+ThX+neI3rQ320djzRABckNeE6e4Kkwzn+QdZsmA2SDvM9IU7boK1jVQlgUPp7zF5q3hbb8Rx7AadyTarBayUkCgNlrMqth+tmTMWttMqCPxJRGnhhvesAHIl55a28Kzz/2Oqa3J9zwzbyDIwlEXho0eAq3YXEPeBhl34k+7gOt/5Zdbh+yacFoxDh0LrshQgboAijcVVaXPeN0LsHEiVvYIzugwIvCkoFMPWoPj/kEGzPY6FCkVneDA7VoLTCoG8dlrN08Lf05/BGC7Wllm66pTNZC/cKXP+cjpQn1iEuiuPxnPldlMHx9sx2y/BRoft6oT/GzqkNy1NTY/xI+MfmxXnF5kwSbcTbzZQ9fZ8xjh/vmpPBgDNrxOEAT4N6OG7GQIhb9HEhXQCQ== example-key", //nolint + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwpOyZjZ4gB0OTvmofH3llh6cBCWaEiEmHZWSkDXr8Bih6HcXVOtYMcFi/ZnUVGUBPw3ATNQBZUaVCYKeF+nDfKTJ9hmnlsyHxV2LeMsVg1o15Pb6f+QJuavEqtE6HI7mHyId4Z1quVTJXDWDW8OZEG7M3VktauqAn/e9UJvlL0bGmTFD1XkNcbRsWMRWkQgt2ozqlgrpPtvrg2/+bNucxX++VUjnsn+fGgAT07kbnrZwppGnAfjbYthxhv7GeSD0+Z0Lf1kiKy/bhUqXsZIuexOfF0YrRyUH1KBl8GCX2OLBYvXHyusByqsrOPiROqRdjX5PsK6HSAS0lk0niTt1p example-key-2", // nolint + }, + NodeSSHPrivateKeys: sshPrivateKeySecretName, }, }, }, }, + Status: airshipv1.SIPClusterStatus{}, }, - Status: airshipv1.SIPClusterStatus{}, - } + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: sshPrivateKeySecretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "key": []byte(sshPrivateKeyBase64), + }, + Type: corev1.SecretTypeOpaque, + } } // CreateBMCAuthSecret creates a K8s Secret that matches the Metal3.io BaremetalHost credential format for use in test