diff --git a/cli/cmd/certificateChain.go b/cli/cmd/certificateChain.go index f3dde5f6..bde39750 100644 --- a/cli/cmd/certificateChain.go +++ b/cli/cmd/certificateChain.go @@ -13,8 +13,8 @@ func newCertificateChain() *cobra.Command { cmd := &cobra.Command{ Use: "chain ", - Short: "returns the certificate chain of the Marblerun coordinator", - Long: `returns the certificate chain of the Marblerun coordinator`, + Short: "Returns the certificate chain of the Marblerun coordinator", + Long: `Returns the certificate chain of the Marblerun coordinator`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { hostName := args[0] diff --git a/cli/cmd/certificateIntermediate.go b/cli/cmd/certificateIntermediate.go index d60c76d7..c18748a7 100644 --- a/cli/cmd/certificateIntermediate.go +++ b/cli/cmd/certificateIntermediate.go @@ -13,8 +13,8 @@ func newCertificateIntermediate() *cobra.Command { cmd := &cobra.Command{ Use: "intermediate ", - Short: "returns the intermediate certificate of the Marblerun coordinator", - Long: `returns the intermediate certificate of the Marblerun coordinator`, + Short: "Returns the intermediate certificate of the Marblerun coordinator", + Long: `Returns the intermediate certificate of the Marblerun coordinator`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { hostName := args[0] diff --git a/cli/cmd/certificateRoot.go b/cli/cmd/certificateRoot.go index 0cd27bf9..42c48ba2 100644 --- a/cli/cmd/certificateRoot.go +++ b/cli/cmd/certificateRoot.go @@ -13,8 +13,8 @@ func newCertificateRoot() *cobra.Command { cmd := &cobra.Command{ Use: "root ", - Short: "returns the root certificate of the Marblerun coordinator", - Long: `returns the root certificate of the Marblerun coordinator`, + Short: "Returns the root certificate of the Marblerun coordinator", + Long: `Returns the root certificate of the Marblerun coordinator`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { hostName := args[0] diff --git a/cli/cmd/csr_test.go b/cli/cmd/csr_test.go new file mode 100644 index 00000000..a415f249 --- /dev/null +++ b/cli/cmd/csr_test.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateCSR(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + testKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + + pem, err := createCSR(testKey) + require.NoError(err) + assert.Equal("CERTIFICATE REQUEST", pem.Type) +} + +func TestCertificateV1(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + testClient := fake.NewSimpleClientset() + + testHandler, err := newCertificateV1(testClient) + require.NoError(err) + + testKey := testHandler.getKey() + assert.Equal(testKey, testHandler.privKey, "private key of the handler and private key returned by its method were not equal") + + testHandler.timeout = 5 + // this should error with a timeout since the fakeClient does not keep upated resources, but only returns them once on API call + err = testHandler.signRequest() + require.Error(err) + assert.Contains(err.Error(), "certificate signing request was not updated", fmt.Sprintf("failed with unexpected error: %s", err.Error())) + + // we use a different timeout function here, so this should not return an error, but the certificate will be empty + testCrt, err := testHandler.get() + require.NoError(err) + assert.True((len(testCrt) == 0)) + + testValues := map[string]interface{}{ + "marbleInjector": map[string]interface{}{ + "start": false, + "CABundle": "string", + }, + } + + testHandler.setCaBundle(testValues) + assert.Equal(nil, testValues["marbleInjector"].(map[string]interface{})["CABundle"], "failed to remove CABundle") + assert.Equal(true, testValues["marbleInjector"].(map[string]interface{})["start"], "failed to set start to true") +} + +func TestCertificateLegacy(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + testHandler, err := newCertificateLegacy() + require.NoError(err) + assert.True(len(testHandler.caCert.Bytes) > 0, "failed creating caCert") + + err = testHandler.signRequest() + require.NoError(err) + assert.True(len(testHandler.serverCert.Bytes) > 0, "failed creating serverCert") + + testKey := testHandler.getKey() + assert.Equal(testKey, testHandler.serverPrivKey, "private key of the handler and private key returned by its method were not equal") + + testCrt, err := testHandler.get() + require.NoError(err) + assert.True(len(testCrt) > 0, "failed to retrieve server certificate") + + testValues := map[string]interface{}{ + "marbleInjector": map[string]interface{}{ + "start": false, + "CABundle": "string", + }, + } + + testHandler.setCaBundle(testValues) + assert.Equal(true, testValues["marbleInjector"].(map[string]interface{})["start"], "failed to set start to true") + assert.NotEqual("string", testValues["marbleInjector"].(map[string]interface{})["CABundle"], "failed to set CABundle") +} diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 0a584cda..6167dcf7 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -239,7 +239,7 @@ func installWebhook(vals map[string]interface{}) error { } // createSecret creates a secret containing the signed certificate and private key for the webhook server -func createSecret(privKey *rsa.PrivateKey, crt []byte, kubeClient *kubernetes.Clientset) error { +func createSecret(privKey *rsa.PrivateKey, crt []byte, kubeClient kubernetes.Interface) error { rsaPEM := pem.EncodeToMemory( &pem.Block{ Type: "RSA PRIVATE KEY", @@ -285,14 +285,14 @@ func getCertificateHandler(kubeClient kubernetes.Interface) (certificateInterfac return newCertificateV1(kubeClient) } -func verifyNamespace(namespace string, kubeClient *kubernetes.Clientset) error { - _, err := kubeClient.CoreV1().Namespaces().Get(context.TODO(), "marblerun", metav1.GetOptions{}) +func verifyNamespace(namespace string, kubeClient kubernetes.Interface) error { + _, err := kubeClient.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) if err != nil { // if the namespace does not exist we create it - if err.Error() == "namespaces \"marblerun\" not found" { + if err.Error() == fmt.Sprintf("namespaces \"%s\" not found", namespace) { marbleNamespace := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: "marblerun", + Name: namespace, }, } if _, err := kubeClient.CoreV1().Namespaces().Create(context.TODO(), marbleNamespace, metav1.CreateOptions{}); err != nil { diff --git a/cli/cmd/install_test.go b/cli/cmd/install_test.go new file mode 100644 index 00000000..2c2a6588 --- /dev/null +++ b/cli/cmd/install_test.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/version" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/kubernetes/fake" +) + +func TestCreateSecret(t *testing.T) { + require := require.New(t) + testClient := fake.NewSimpleClientset() + + testKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + crt := []byte{0xAA, 0xAA, 0xAA} + + newNamespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "marblerun", + }, + } + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace1, metav1.CreateOptions{}) + require.NoError(err) + + err = createSecret(testKey, crt, testClient) + require.NoError(err) + _, err = testClient.CoreV1().Secrets("marblerun").Get(context.TODO(), "marble-injector-webhook-certs", metav1.GetOptions{}) + require.NoError(err) + + // we should get an error since the secret was already created in the previous step + err = createSecret(testKey, crt, testClient) + require.Error(err) +} + +func TestGetCertificateHandler(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + testClient := fake.NewSimpleClientset() + + testClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{ + Major: "1", + Minor: "19", + } + testHandler, err := getCertificateHandler(testClient) + require.NoError(err) + assert.Equal(reflect.TypeOf(testHandler).String(), "*cmd.certificateV1") + + testClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{ + Major: "1", + Minor: "18", + } + testHandler, err = getCertificateHandler(testClient) + require.NoError(err) + assert.Equal(reflect.TypeOf(testHandler).String(), "*cmd.certificateLegacy") +} + +func TestVerifyNamespace(t *testing.T) { + require := require.New(t) + testClient := fake.NewSimpleClientset() + + _, err := testClient.CoreV1().Namespaces().Get(context.TODO(), "test-space", metav1.GetOptions{}) + require.Error(err) + + // namespace does not exist, it should be created here + err = verifyNamespace("test-space", testClient) + require.NoError(err) + + _, err = testClient.CoreV1().Namespaces().Get(context.TODO(), "test-space", metav1.GetOptions{}) + require.NoError(err) + + // namespace exists, should return nil + err = verifyNamespace("test-space", testClient) + require.NoError(err) +} diff --git a/cli/cmd/namespace_test.go b/cli/cmd/namespace_test.go new file mode 100644 index 00000000..ef3cc8f2 --- /dev/null +++ b/cli/cmd/namespace_test.go @@ -0,0 +1,161 @@ +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNameSpaceAdd(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + testClient := fake.NewSimpleClientset() + + // Test adding non existant namespace + err := cliNameSpaceAdd([]string{"test-space-1"}, testClient, true) + require.Error(err) + + // Test adding multiple non existant namespaces + err = cliNameSpaceAdd([]string{"test-space-1", "test-space-2", "test-space-3"}, testClient, true) + require.Error(err) + + // Create namespace to add to mesh + newNamespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-space-1", + }, + } + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace1, metav1.CreateOptions{}) + require.NoError(err) + err = cliNameSpaceAdd([]string{"test-space-1"}, testClient, true) + require.NoError(err) + + injectedNamespace, err := testClient.CoreV1().Namespaces().Get(context.TODO(), "test-space-1", metav1.GetOptions{}) + require.NoError(err) + assert.Equal(injectedNamespace.Labels["marblerun/inject"], "enabled", "failed to inject marblerun label") + assert.Equal(injectedNamespace.Labels["marblerun/inject-sgx"], "disabled", "injected sgx label when it shouldnt have") + + // Create two more namespaces + newNamespace2 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-space-2", + }, + } + newNamespace3 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-space-3", + }, + } + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace2, metav1.CreateOptions{}) + require.NoError(err) + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace3, metav1.CreateOptions{}) + require.NoError(err) + err = cliNameSpaceAdd([]string{"test-space-2", "test-space-3"}, testClient, false) + require.NoError(err) + + injectedNamespace, err = testClient.CoreV1().Namespaces().Get(context.TODO(), "test-space-2", metav1.GetOptions{}) + require.NoError(err) + assert.Equal(injectedNamespace.Labels["marblerun/inject"], "enabled", "failed to inject marblerun label") + assert.Equal(injectedNamespace.Labels["marblerun/inject-sgx"], "enabled", "failed to inject marblerun inject-sgx label") + + injectedNamespace, err = testClient.CoreV1().Namespaces().Get(context.TODO(), "test-space-3", metav1.GetOptions{}) + require.NoError(err) + assert.Equal(injectedNamespace.Labels["marblerun/inject"], "enabled", "failed to inject marblerun label") + assert.Equal(injectedNamespace.Labels["marblerun/inject-sgx"], "enabled", "failed to inject marblerun inject-sgx label") +} + +func TestNameSpaceList(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + testClient := fake.NewSimpleClientset() + + // Test listing on empty + err := cliNameSpaceList(testClient) + require.NoError(err) + + // Create and add two namespaces + newNamespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-space-1", + Labels: map[string]string{ + "marblerun/inject": "enabled", + "marblerun/inject-sgx": "enabled", + }, + }, + } + newNamespace2 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-space-2", + Labels: map[string]string{ + "marblerun/inject": "enabled", + "marblerun/inject-sgx": "disabled", + }, + }, + } + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace1, metav1.CreateOptions{}) + require.NoError(err) + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace2, metav1.CreateOptions{}) + require.NoError(err) + + list, err := selectNamespaces(testClient) + require.NoError(err) + assert.Equal(len(list.Items), 2, fmt.Sprintf("expected 2 items but got %d", len(list.Items))) + + err = cliNameSpaceList(testClient) + require.NoError(err) +} + +func TestNameSpaceRemove(t *testing.T) { + require := require.New(t) + //assert := assert.New(t) + + testClient := fake.NewSimpleClientset() + + // Try removing non existant namespace + err := cliNameSpaceRemove("test-space-1", testClient) + require.Error(err) + + newNamespace1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-space-1", + Labels: map[string]string{ + "marblerun/inject": "enabled", + "marblerun/inject-sgx": "enabled", + }, + }, + } + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace1, metav1.CreateOptions{}) + require.NoError(err) + + // Remove namespace from mesh + err = cliNameSpaceRemove("test-space-1", testClient) + require.NoError(err) + + // Try removing namespace that is not labeled + err = cliNameSpaceRemove("test-space-1", testClient) + require.Error(err) + + newNamespace2 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-space-2", + Labels: map[string]string{ + "marblerun/inject": "wrong-value", + "marblerun/inject-sgx": "enabled", + }, + }, + } + _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace2, metav1.CreateOptions{}) + require.NoError(err) + + // Try removing namespace with incorrect label + err = cliNameSpaceRemove("test-space-2", testClient) + require.Error(err) +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 79bb2290..fb535256 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -4,10 +4,18 @@ import ( "github.com/spf13/cobra" ) +var globalUsage = `The marblerun CLI enables you to install and manage the Marblerun +confidential computing service mesh in your Kubernetes cluster + +To install and configure Marblerun, run: + + $ marblerun install +` + var rootCmd = &cobra.Command{ Use: "marblerun", - Short: "marblerun cli short description", - Long: `marblerun cli long description`, + Short: "Install and manage the Marblerun confidential computing service mesh", + Long: globalUsage, } // Execute starts the CLI