diff --git a/README.md b/README.md index 883d08a5..75777c05 100644 --- a/README.md +++ b/README.md @@ -144,25 +144,24 @@ When using a `Development` license, the Replicated SDK will initiate in integrat ```yaml integration: licenseID: "development-license-id" - mock: - enabled: true - data: | - helmChartURL: oci://registry.replicated.com/dev-app/dev-channel/dev-parent-chart - currentRelease: - versionLabel: 0.1.7 - releaseNotes: "test" - createdAt: "2012-09-09" - helmReleaseName: dev-parent-chart - helmReleaseRevision: 2 - helmReleaseNamespace: default + enabled: true + mockData: | + helmChartURL: oci://registry.replicated.com/dev-app/dev-channel/dev-parent-chart + currentRelease: + versionLabel: 0.1.7 + releaseNotes: "test" + createdAt: "2012-09-09" + helmReleaseName: dev-parent-chart + helmReleaseRevision: 2 + helmReleaseNamespace: default ``` To enable the Replicated SDK's `integration` mode, you can use the following values in the chart YAML: - `licenseID`: This should be set to the development license ID obtained from the vendor portal. -- `mock.enabled`: When this field is set to `true`, the SDK will return mocked data. -- `mock.data`: This field allows you to override the default mock data the Replicated SDK returns when `mock.enabled` is set to `true`. +- `enabled`: When this field is set to `true`, the SDK will return mocked data. +- `mockData`: This field allows you to override the default mock data the Replicated SDK returns when `integration.enabled` is set to `true`. -Below is an example demonstrating all the supported values for the `mock.data` field: +Below is an example demonstrating all the supported values for the `mockData` field: ```yaml helmChartURL: oci://registry.replicated.com/dev-app/dev-channel/dev-parent-chart currentRelease: @@ -230,9 +229,8 @@ e.g.: for staging licenses you can set the replicated app endpoint as below in ` ```yaml replicatedAppEndpoint: "https://staging.replicated.app" integration: - licenseID: "development-license-id" - mock: - enabled: true + licenseID: "staging-license-id" + enabled: true ``` ## Testing diff --git a/chart/templates/replicated-secret.yaml b/chart/templates/replicated-secret.yaml index b37232b2..59a440b3 100644 --- a/chart/templates/replicated-secret.yaml +++ b/chart/templates/replicated-secret.yaml @@ -32,9 +32,9 @@ stringData: REPLICATED_INTEGRATION_LICENSE_ID: {{ .Values.integration.licenseID }} {{- end }} # kindIs "invalid" indicates that the value is nil and the user did not provide a value - {{- if not (kindIs "invalid" ((.Values.integration).mock).enabled) }} - REPLICATED_MOCK_ENABLED: "{{ ((.Values.integration).mock).enabled }}" + {{- if not (kindIs "invalid" (.Values.integration).enabled) }} + REPLICATED_INTEGRATION_ENABLED: "{{ (.Values.integration).enabled }}" {{- end }} - {{- if ((.Values.integration).mock).data }} - REPLICATED_MOCK_DATA: {{- .Values.integration.mock.data | toYaml | indent 1 }} + {{- if (.Values.integration).mockData }} + REPLICATED_INTEGRATION_MOCK_DATA: {{- .Values.integration.mockData | toYaml | indent 1 }} {{- end }} diff --git a/chart/values.yaml.tmpl b/chart/values.yaml.tmpl index 03a52bd1..177016d1 100644 --- a/chart/values.yaml.tmpl +++ b/chart/values.yaml.tmpl @@ -40,6 +40,5 @@ service: # "integration" mode related values. integration: licenseID: "" - mock: - # enabled: false - data: "" + # enabled: false + mockData: "" diff --git a/pkg/apiserver/bootstrap.go b/pkg/apiserver/bootstrap.go index fb9bcfcf..bbd86420 100644 --- a/pkg/apiserver/bootstrap.go +++ b/pkg/apiserver/bootstrap.go @@ -7,9 +7,9 @@ import ( appstatetypes "github.com/replicatedhq/replicated-sdk/pkg/appstate/types" "github.com/replicatedhq/replicated-sdk/pkg/heartbeat" "github.com/replicatedhq/replicated-sdk/pkg/helm" + "github.com/replicatedhq/replicated-sdk/pkg/integration" "github.com/replicatedhq/replicated-sdk/pkg/k8sutil" sdklicense "github.com/replicatedhq/replicated-sdk/pkg/license" - "github.com/replicatedhq/replicated-sdk/pkg/mock" "github.com/replicatedhq/replicated-sdk/pkg/store" "github.com/replicatedhq/replicated-sdk/pkg/upstream" upstreamtypes "github.com/replicatedhq/replicated-sdk/pkg/upstream/types" @@ -81,16 +81,12 @@ func bootstrap(params APIServerParams) error { return errors.Wrap(err, "failed to get clientset") } - if store.GetStore().IsDevLicense() { - mock.InitMock(clientset, store.GetStore().GetNamespace()) - } - - isMockEnabled, err := mock.MustGetMock().IsMockEnabled(params.Context, store.GetStore().GetLicense()) + isIntegrationModeEnabled, err := integration.IsEnabled(params.Context, clientset, store.GetStore().GetNamespace(), store.GetStore().GetLicense()) if err != nil { - return errors.Wrap(err, "failed to check if mock is enabled") + return errors.Wrap(err, "failed to check if integration mode is enabled") } - if !util.IsAirgap() && !isMockEnabled { + if !util.IsAirgap() && !isIntegrationModeEnabled { // retrieve and cache updates currentCursor := upstreamtypes.ReplicatedCursor{ ChannelID: store.GetStore().GetChannelID(), diff --git a/pkg/handlers/app.go b/pkg/handlers/app.go index fea62241..aeface8d 100644 --- a/pkg/handlers/app.go +++ b/pkg/handlers/app.go @@ -9,9 +9,11 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/replicated-sdk/pkg/config" "github.com/replicatedhq/replicated-sdk/pkg/helm" + "github.com/replicatedhq/replicated-sdk/pkg/integration" + integrationtypes "github.com/replicatedhq/replicated-sdk/pkg/integration/types" + "github.com/replicatedhq/replicated-sdk/pkg/k8sutil" sdklicense "github.com/replicatedhq/replicated-sdk/pkg/license" "github.com/replicatedhq/replicated-sdk/pkg/logger" - "github.com/replicatedhq/replicated-sdk/pkg/mock" "github.com/replicatedhq/replicated-sdk/pkg/store" "github.com/replicatedhq/replicated-sdk/pkg/upstream" upstreamtypes "github.com/replicatedhq/replicated-sdk/pkg/upstream/types" @@ -42,20 +44,27 @@ type AppRelease struct { } func GetCurrentAppInfo(w http.ResponseWriter, r *http.Request) { - isMockEnabled, err := mock.MustGetMock().IsMockEnabled(r.Context(), store.GetStore().GetLicense()) + clientset, err := k8sutil.GetClientset() if err != nil { - logger.Errorf("failed to check if mock is enabled: %v", err) + logger.Error(errors.Wrap(err, "failed to get clientset")) w.WriteHeader(http.StatusInternalServerError) return } - if isMockEnabled { + isIntegrationModeEnabled, err := integration.IsEnabled(r.Context(), clientset, store.GetStore().GetNamespace(), store.GetStore().GetLicense()) + if err != nil { + logger.Errorf("failed to check if integration mode is enabled: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if isIntegrationModeEnabled { response := GetCurrentAppInfoResponse{ AppSlug: store.GetStore().GetAppSlug(), AppName: store.GetStore().GetAppName(), } - mockCurrentRelease, err := mock.MustGetMock().GetCurrentRelease(r.Context()) + mockCurrentRelease, err := integration.GetCurrentRelease(r.Context(), clientset, store.GetStore().GetNamespace()) if err != nil { logger.Errorf("failed to get mock current release: %v", err) w.WriteHeader(http.StatusInternalServerError) @@ -65,7 +74,7 @@ func GetCurrentAppInfo(w http.ResponseWriter, r *http.Request) { response.CurrentRelease = mockReleaseToAppRelease(*mockCurrentRelease) } - mockHelmChartURL, err := mock.MustGetMock().GetHelmChartURL(r.Context()) + mockHelmChartURL, err := integration.GetHelmChartURL(r.Context(), clientset, store.GetStore().GetNamespace()) if err != nil { logger.Errorf("failed to get mock helm chart url: %v", err) w.WriteHeader(http.StatusInternalServerError) @@ -106,15 +115,22 @@ func GetCurrentAppInfo(w http.ResponseWriter, r *http.Request) { } func GetAppUpdates(w http.ResponseWriter, r *http.Request) { - isMockEnabled, err := mock.MustGetMock().IsMockEnabled(r.Context(), store.GetStore().GetLicense()) + clientset, err := k8sutil.GetClientset() if err != nil { - logger.Errorf("failed to check if mock is enabled: %v", err) + logger.Error(errors.Wrap(err, "failed to get clientset")) w.WriteHeader(http.StatusInternalServerError) return } - if isMockEnabled { - mockAvailableReleases, err := mock.MustGetMock().GetAvailableReleases(r.Context()) + isIntegrationModeEnabled, err := integration.IsEnabled(r.Context(), clientset, store.GetStore().GetNamespace(), store.GetStore().GetLicense()) + if err != nil { + logger.Errorf("failed to check if integration mode is enabled: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if isIntegrationModeEnabled { + mockAvailableReleases, err := integration.GetAvailableReleases(r.Context(), clientset, store.GetStore().GetNamespace()) if err != nil { logger.Errorf("failed to get available mock releases: %v", err) w.WriteHeader(http.StatusInternalServerError) @@ -166,15 +182,22 @@ func GetAppUpdates(w http.ResponseWriter, r *http.Request) { } func GetAppHistory(w http.ResponseWriter, r *http.Request) { - isMockEnabled, err := mock.MustGetMock().IsMockEnabled(r.Context(), store.GetStore().GetLicense()) + clientset, err := k8sutil.GetClientset() + if err != nil { + logger.Error(errors.Wrap(err, "failed to get clientset")) + w.WriteHeader(http.StatusInternalServerError) + return + } + + isIntegrationModeEnabled, err := integration.IsEnabled(r.Context(), clientset, store.GetStore().GetNamespace(), store.GetStore().GetLicense()) if err != nil { - logger.Errorf("failed to check if mock is enabled: %v", err) + logger.Errorf("failed to check if integration mode is enabled: %v", err) w.WriteHeader(http.StatusInternalServerError) return } - if isMockEnabled { - mockReleases, err := mock.MustGetMock().GetDeployedReleases(r.Context()) + if isIntegrationModeEnabled { + mockReleases, err := integration.GetDeployedReleases(r.Context(), clientset, store.GetStore().GetNamespace()) if err != nil { logger.Errorf("failed to get mock releases: %v", err) w.WriteHeader(http.StatusInternalServerError) @@ -278,7 +301,7 @@ func helmReleaseToAppRelease(helmRelease *helmrelease.Release) *AppRelease { return nil } -func mockReleaseToAppRelease(mockRelease mock.MockRelease) AppRelease { +func mockReleaseToAppRelease(mockRelease integrationtypes.MockRelease) AppRelease { appRelease := AppRelease{ VersionLabel: mockRelease.VersionLabel, ReleaseNotes: mockRelease.ReleaseNotes, diff --git a/pkg/handlers/mock.go b/pkg/handlers/mock.go index 0972fc3c..f3730739 100644 --- a/pkg/handlers/mock.go +++ b/pkg/handlers/mock.go @@ -4,20 +4,42 @@ import ( "encoding/json" "net/http" + "github.com/pkg/errors" + "github.com/replicatedhq/replicated-sdk/pkg/integration" + integrationtypes "github.com/replicatedhq/replicated-sdk/pkg/integration/types" + "github.com/replicatedhq/replicated-sdk/pkg/k8sutil" "github.com/replicatedhq/replicated-sdk/pkg/logger" - "github.com/replicatedhq/replicated-sdk/pkg/mock" "github.com/replicatedhq/replicated-sdk/pkg/store" ) func PostMockData(w http.ResponseWriter, r *http.Request) { - mockDataRequest := mock.MockData{} + clientset, err := k8sutil.GetClientset() + if err != nil { + logger.Error(errors.Wrap(err, "failed to get clientset")) + w.WriteHeader(http.StatusInternalServerError) + return + } + + isIntegrationModeEnabled, err := integration.IsEnabled(r.Context(), clientset, store.GetStore().GetNamespace(), store.GetStore().GetLicense()) + if err != nil { + logger.Errorf("failed to check if integration mode is enabled: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if !isIntegrationModeEnabled { + w.WriteHeader(http.StatusForbidden) + return + } + + mockDataRequest := integrationtypes.MockData{} if err := json.NewDecoder(r.Body).Decode(&mockDataRequest); err != nil { logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusBadRequest) return } - if err := mock.MustGetMock().SetMockData(r.Context(), mockDataRequest); err != nil { + if err := integration.SetMockData(r.Context(), clientset, store.GetStore().GetNamespace(), mockDataRequest); err != nil { logger.Errorf("failed to update mock data: %v", err) w.WriteHeader(http.StatusInternalServerError) return @@ -27,12 +49,26 @@ func PostMockData(w http.ResponseWriter, r *http.Request) { } func GetMockData(w http.ResponseWriter, r *http.Request) { - if !store.GetStore().IsDevLicense() { + clientset, err := k8sutil.GetClientset() + if err != nil { + logger.Error(errors.Wrap(err, "failed to get clientset")) + w.WriteHeader(http.StatusInternalServerError) + return + } + + isIntegrationModeEnabled, err := integration.IsEnabled(r.Context(), clientset, store.GetStore().GetNamespace(), store.GetStore().GetLicense()) + if err != nil { + logger.Errorf("failed to check if integration mode is enabled: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if !isIntegrationModeEnabled { w.WriteHeader(http.StatusForbidden) return } - mockData, err := mock.MustGetMock().GetMockData(r.Context()) + mockData, err := integration.GetMockData(r.Context(), clientset, store.GetStore().GetNamespace()) if err != nil { logger.Errorf("failed to get mock data: %v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/mock/default_mock_data.yaml b/pkg/integration/data/default_mock_data.yaml similarity index 100% rename from pkg/mock/default_mock_data.yaml rename to pkg/integration/data/default_mock_data.yaml diff --git a/pkg/mock/test_mock_data.yaml b/pkg/integration/data/test_mock_data.yaml similarity index 100% rename from pkg/mock/test_mock_data.yaml rename to pkg/integration/data/test_mock_data.yaml diff --git a/pkg/integration/integration.go b/pkg/integration/integration.go new file mode 100644 index 00000000..585e26ab --- /dev/null +++ b/pkg/integration/integration.go @@ -0,0 +1,68 @@ +package integration + +import ( + "context" + "strconv" + "sync" + + "github.com/pkg/errors" + kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1" + corev1 "k8s.io/api/core/v1" + kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + replicatedSecretName = "replicated" + replicatedIntegrationMockDataKey = "REPLICATED_INTEGRATION_MOCK_DATA" + replicatedIntegrationEnabledKey = "REPLICATED_INTEGRATION_ENABLED" +) + +var replicatedSecretLock = sync.Mutex{} + +func IsEnabled(ctx context.Context, clientset kubernetes.Interface, namespace string, license *kotsv1beta1.License) (bool, error) { + if license == nil || license.Spec.LicenseType != "dev" { + return false, nil + } + + replicatedSecretLock.Lock() + defer replicatedSecretLock.Unlock() + + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, replicatedSecretName, metav1.GetOptions{}) + if err != nil { + if kuberneteserrors.IsNotFound(err) { + return false, nil + } + return false, errors.Wrap(err, "failed to get replicated secret") + } + + v, ok := secret.Data[replicatedIntegrationEnabledKey] + if !ok || len(v) == 0 { + return true, nil + } + + enabled, _ := strconv.ParseBool(string(v)) + return enabled, nil +} + +func createReplicatedSecret(ctx context.Context, clientset kubernetes.Interface, namespace string, data map[string][]byte) error { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: replicatedSecretName, + Namespace: namespace, + }, + Data: data, + } + + _, err := clientset.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + return errors.Wrap(err, "failed to create secret replicated") + } + + return nil +} diff --git a/pkg/integration/integration_test.go b/pkg/integration/integration_test.go new file mode 100644 index 00000000..717a97d8 --- /dev/null +++ b/pkg/integration/integration_test.go @@ -0,0 +1,181 @@ +package integration + +import ( + "context" + "testing" + + kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +func TestIntegration_IsEnabled(t *testing.T) { + type args struct { + clientset kubernetes.Interface + namespace string + license *kotsv1beta1.License + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "is not enabled", + args: args{ + clientset: fake.NewSimpleClientset(), + namespace: "default", + }, + want: false, + wantErr: false, + }, + { + name: "is enabled", + args: args{ + clientset: fake.NewSimpleClientset(&corev1.SecretList{ + TypeMeta: metav1.TypeMeta{}, + ListMeta: metav1.ListMeta{}, + Items: []corev1.Secret{{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: replicatedSecretName, + Namespace: "default", + }, + Data: map[string][]byte{ + replicatedIntegrationEnabledKey: []byte("true"), + }, + }}, + }), + namespace: "default", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + LicenseType: "dev", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "not enabled because not a dev license", + args: args{ + clientset: fake.NewSimpleClientset(&corev1.SecretList{ + TypeMeta: metav1.TypeMeta{}, + ListMeta: metav1.ListMeta{}, + Items: []corev1.Secret{{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: replicatedSecretName, + Namespace: "default", + }, + Data: map[string][]byte{ + replicatedIntegrationEnabledKey: []byte("true"), + }, + }}, + }), + namespace: "default", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + LicenseType: "paid", + }, + }, + }, + want: false, + wantErr: false, + }, + { + name: "not enabled for a dev license", + args: args{ + clientset: fake.NewSimpleClientset(&corev1.SecretList{ + TypeMeta: metav1.TypeMeta{}, + ListMeta: metav1.ListMeta{}, + Items: []corev1.Secret{{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: replicatedSecretName, + Namespace: "default", + }, + Data: map[string][]byte{ + replicatedIntegrationEnabledKey: []byte("false"), + }, + }}, + }), + namespace: "default", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + LicenseType: "dev", + }, + }, + }, + want: false, + wantErr: false, + }, + { + name: "enabled for a dev license because key doesn't exist", + args: args{ + clientset: fake.NewSimpleClientset(&corev1.SecretList{ + TypeMeta: metav1.TypeMeta{}, + ListMeta: metav1.ListMeta{}, + Items: []corev1.Secret{{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: replicatedSecretName, + Namespace: "default", + }, + Data: map[string][]byte{}, + }}, + }), + namespace: "default", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + LicenseType: "dev", + }, + }, + }, + want: true, + wantErr: false, + }, + { + name: "enabled for a dev license because value is empty", + args: args{ + clientset: fake.NewSimpleClientset(&corev1.SecretList{ + TypeMeta: metav1.TypeMeta{}, + ListMeta: metav1.ListMeta{}, + Items: []corev1.Secret{{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: replicatedSecretName, + Namespace: "default", + }, + Data: map[string][]byte{ + replicatedIntegrationEnabledKey: []byte(""), + }, + }}, + }), + namespace: "default", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + LicenseType: "dev", + }, + }, + }, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := IsEnabled(context.Background(), tt.args.clientset, tt.args.namespace, tt.args.license) + if (err != nil) != tt.wantErr { + t.Errorf("IsEnabled() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IsEnabled() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/integration/mock.go b/pkg/integration/mock.go new file mode 100644 index 00000000..4e6fdd28 --- /dev/null +++ b/pkg/integration/mock.go @@ -0,0 +1,130 @@ +package integration + +import ( + "context" + _ "embed" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated-sdk/pkg/integration/types" + "gopkg.in/yaml.v2" + kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +var ( + //go:embed data/default_mock_data.yaml + defaultMockDataYAML []byte +) + +func GetHelmChartURL(ctx context.Context, clientset kubernetes.Interface, namespace string) (string, error) { + mockData, err := GetMockData(ctx, clientset, namespace) + if err != nil { + return "", errors.Wrap(err, "failed to get mock data") + } else if mockData == nil { + return "", nil + } + + return mockData.HelmChartURL, nil +} + +func GetCurrentRelease(ctx context.Context, clientset kubernetes.Interface, namespace string) (*types.MockRelease, error) { + mockData, err := GetMockData(ctx, clientset, namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get mock data") + } else if mockData == nil { + return nil, nil + } + + return mockData.CurrentRelease, nil +} + +func GetAvailableReleases(ctx context.Context, clientset kubernetes.Interface, namespace string) ([]types.MockRelease, error) { + mockData, err := GetMockData(ctx, clientset, namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get mock data") + } else if mockData == nil { + return nil, nil + } + + return mockData.AvailableReleases, nil +} + +func GetDeployedReleases(ctx context.Context, clientset kubernetes.Interface, namespace string) ([]types.MockRelease, error) { + mockData, err := GetMockData(ctx, clientset, namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get mock data") + } else if mockData == nil { + return nil, nil + } + + return mockData.DeployedReleases, nil +} + +func SetMockData(ctx context.Context, clientset kubernetes.Interface, namespace string, mockData types.MockData) error { + replicatedSecretLock.Lock() + defer replicatedSecretLock.Unlock() + + b, err := yaml.Marshal(mockData) + if err != nil { + return errors.Wrap(err, "failed to marshal mock data") + } + + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, replicatedSecretName, metav1.GetOptions{}) + if err != nil { + if kuberneteserrors.IsNotFound(err) { + data := map[string][]byte{ + replicatedIntegrationMockDataKey: b, + } + err = createReplicatedSecret(ctx, clientset, namespace, data) + if err != nil { + return errors.Wrap(err, "failed to create secret replicated") + } + return nil + } + + return errors.Wrap(err, "failed to get replicated secret") + } + + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + + secret.Data[replicatedIntegrationMockDataKey] = b + _, err = clientset.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}) + if err != nil { + return errors.Wrap(err, "failed to update replicated secret") + } + + return nil +} + +func GetMockData(ctx context.Context, clientset kubernetes.Interface, namespace string) (*types.MockData, error) { + replicatedSecretLock.Lock() + defer replicatedSecretLock.Unlock() + + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, replicatedSecretName, metav1.GetOptions{}) + if err != nil && !kuberneteserrors.IsNotFound(err) { + return nil, errors.Wrap(err, "failed to get replicated secret") + } + if err == nil { + b := secret.Data[replicatedIntegrationMockDataKey] + if len(b) != 0 { + var mockData types.MockData + if err := yaml.Unmarshal(b, &mockData); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal mock data") + } + return &mockData, nil + } + } + + return GetDefaultMockData(ctx) +} + +func GetDefaultMockData(ctx context.Context) (*types.MockData, error) { + var mockData types.MockData + if err := yaml.Unmarshal(defaultMockDataYAML, &mockData); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal default mock data") + } + return &mockData, nil +} diff --git a/pkg/mock/mock_test.go b/pkg/integration/mock_test.go similarity index 60% rename from pkg/mock/mock_test.go rename to pkg/integration/mock_test.go index 72593d8d..bcb57545 100644 --- a/pkg/mock/mock_test.go +++ b/pkg/integration/mock_test.go @@ -1,4 +1,4 @@ -package mock +package integration import ( "context" @@ -9,7 +9,7 @@ import ( "testing" "github.com/pmezard/go-difflib/difflib" - kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1" + integrationtypes "github.com/replicatedhq/replicated-sdk/pkg/integration/types" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" @@ -18,137 +18,9 @@ import ( "k8s.io/client-go/kubernetes/fake" ) -//go:embed test_mock_data.yaml +//go:embed data/test_mock_data.yaml var testMockDataYAML []byte -func TestMock_IsMockEnabled(t *testing.T) { - type fields struct { - clientset kubernetes.Interface - namespace string - } - type args struct { - license *kotsv1beta1.License - } - tests := []struct { - name string - fields fields - args args - want bool - wantErr bool - }{ - { - name: "is not enabled", - fields: fields{ - clientset: fake.NewSimpleClientset(), - namespace: "default", - }, - want: false, - wantErr: false, - }, - { - name: "is enabled", - fields: fields{ - clientset: fake.NewSimpleClientset(&corev1.SecretList{ - TypeMeta: metav1.TypeMeta{}, - ListMeta: metav1.ListMeta{}, - Items: []corev1.Secret{{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: replicatedSecretName, - Namespace: "default", - }, - Data: map[string][]byte{ - replicatedMockEnabledKey: []byte("true"), - }, - }}, - }), - namespace: "default", - }, - args: args{ - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - LicenseType: "dev", - }, - }, - }, - want: true, - wantErr: false, - }, - { - name: "not enabled because not a dev license", - fields: fields{ - clientset: fake.NewSimpleClientset(&corev1.SecretList{ - TypeMeta: metav1.TypeMeta{}, - ListMeta: metav1.ListMeta{}, - Items: []corev1.Secret{{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: replicatedSecretName, - Namespace: "default", - }, - Data: map[string][]byte{ - replicatedMockEnabledKey: []byte("true"), - }, - }}, - }), - namespace: "default", - }, - args: args{ - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - LicenseType: "paid", - }, - }, - }, - want: false, - wantErr: false, - }, - { - name: "not enabled for a dev license", - fields: fields{ - clientset: fake.NewSimpleClientset(&corev1.SecretList{ - TypeMeta: metav1.TypeMeta{}, - ListMeta: metav1.ListMeta{}, - Items: []corev1.Secret{{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: replicatedSecretName, - Namespace: "default", - }, - Data: map[string][]byte{ - replicatedMockEnabledKey: []byte("false"), - }, - }}, - }), - namespace: "default", - }, - args: args{ - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - LicenseType: "dev", - }, - }, - }, - want: false, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - InitMock(tt.fields.clientset, tt.fields.namespace) - - got, err := MustGetMock().IsMockEnabled(context.Background(), tt.args.license) - if (err != nil) != tt.wantErr { - t.Errorf("Mock.IsMockEnabled() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("Mock.IsMockEnabled() = %v, want %v", got, tt.want) - } - }) - } -} - func TestMock_GetHelmChartURL(t *testing.T) { defaultMockData, err := GetDefaultMockData(context.Background()) require.NoError(t, err) @@ -188,7 +60,7 @@ func TestMock_GetHelmChartURL(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - replicatedMockDataKey: []byte(testMockDataYAML), + replicatedIntegrationMockDataKey: []byte(testMockDataYAML), }, }}, }), @@ -200,15 +72,13 @@ func TestMock_GetHelmChartURL(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - InitMock(tt.fields.clientset, tt.fields.namespace) - - got, err := MustGetMock().GetHelmChartURL(context.Background()) + got, err := GetHelmChartURL(context.Background(), tt.fields.clientset, tt.fields.namespace) if (err != nil) != tt.wantErr { - t.Errorf("Mock.GetHelmChartURL() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetHelmChartURL() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("Mock.GetHelmChartURL() = %v, want %v", got, tt.want) + t.Errorf("GetHelmChartURL() = %v, want %v", got, tt.want) } }) } @@ -228,7 +98,7 @@ func TestMock_GetCurrentRelease(t *testing.T) { tests := []struct { name string fields fields - want *MockRelease + want *integrationtypes.MockRelease wantErr bool }{ { @@ -253,7 +123,7 @@ func TestMock_GetCurrentRelease(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - replicatedMockDataKey: []byte(testMockDataYAML), + replicatedIntegrationMockDataKey: []byte(testMockDataYAML), }, }}, }), @@ -265,15 +135,13 @@ func TestMock_GetCurrentRelease(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - InitMock(tt.fields.clientset, tt.fields.namespace) - - got, err := MustGetMock().GetCurrentRelease(context.Background()) + got, err := GetCurrentRelease(context.Background(), tt.fields.clientset, tt.fields.namespace) if (err != nil) != tt.wantErr { - t.Errorf("Mock.GetCurrentRelease() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetCurrentRelease() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Mock.GetCurrentRelease() \n\n%s", fmtJSONDiff(got, tt.want)) + t.Errorf("GetCurrentRelease() \n\n%s", fmtJSONDiff(got, tt.want)) } }) } @@ -293,7 +161,7 @@ func TestMock_GetAvailableReleases(t *testing.T) { tests := []struct { name string fields fields - want []MockRelease + want []integrationtypes.MockRelease wantErr bool }{ { @@ -318,7 +186,7 @@ func TestMock_GetAvailableReleases(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - replicatedMockDataKey: []byte(testMockDataYAML), + replicatedIntegrationMockDataKey: []byte(testMockDataYAML), }, }}, }), @@ -330,15 +198,13 @@ func TestMock_GetAvailableReleases(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - InitMock(tt.fields.clientset, tt.fields.namespace) - - got, err := MustGetMock().GetAvailableReleases(context.Background()) + got, err := GetAvailableReleases(context.Background(), tt.fields.clientset, tt.fields.namespace) if (err != nil) != tt.wantErr { - t.Errorf("Mock.GetAvailableReleases() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetAvailableReleases() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Mock.GetAvailableReleases() \n\n%s", fmtJSONDiff(got, tt.want)) + t.Errorf("GetAvailableReleases() \n\n%s", fmtJSONDiff(got, tt.want)) } }) } @@ -358,7 +224,7 @@ func TestMock_GetDeployedReleases(t *testing.T) { tests := []struct { name string fields fields - want []MockRelease + want []integrationtypes.MockRelease wantErr bool }{ { @@ -383,7 +249,7 @@ func TestMock_GetDeployedReleases(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - replicatedMockDataKey: []byte(testMockDataYAML), + replicatedIntegrationMockDataKey: []byte(testMockDataYAML), }, }}, }), @@ -395,15 +261,13 @@ func TestMock_GetDeployedReleases(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - InitMock(tt.fields.clientset, tt.fields.namespace) - - got, err := MustGetMock().GetDeployedReleases(context.Background()) + got, err := GetDeployedReleases(context.Background(), tt.fields.clientset, tt.fields.namespace) if (err != nil) != tt.wantErr { - t.Errorf("Mock.GetDeployedReleases() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetDeployedReleases() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Mock.GetDeployedReleases() \n\n%s", fmtJSONDiff(got, tt.want)) + t.Errorf("GetDeployedReleases() \n\n%s", fmtJSONDiff(got, tt.want)) } }) } @@ -423,7 +287,7 @@ func TestMock_GetMockData(t *testing.T) { tests := []struct { name string fields fields - want *MockData + want *integrationtypes.MockData wantErr bool }{ { @@ -448,7 +312,7 @@ func TestMock_GetMockData(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - replicatedMockDataKey: []byte(testMockDataYAML), + replicatedIntegrationMockDataKey: []byte(testMockDataYAML), }, }}, }), @@ -460,15 +324,13 @@ func TestMock_GetMockData(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - InitMock(tt.fields.clientset, tt.fields.namespace) - - got, err := MustGetMock().GetMockData(context.Background()) + got, err := GetMockData(context.Background(), tt.fields.clientset, tt.fields.namespace) if (err != nil) != tt.wantErr { - t.Errorf("Mock.GetMockData() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetMockData() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Mock.GetMockData() \n\n%s", fmtJSONDiff(got, tt.want)) + t.Errorf("GetMockData() \n\n%s", fmtJSONDiff(got, tt.want)) } }) } @@ -483,13 +345,13 @@ func TestMock_SetMockData(t *testing.T) { namespace string } type args struct { - mockData *MockData + mockData *integrationtypes.MockData } tests := []struct { name string fields fields args args - want *MockData + want *integrationtypes.MockData wantErr bool }{ { @@ -507,30 +369,28 @@ func TestMock_SetMockData(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - InitMock(tt.fields.clientset, tt.fields.namespace) - - err := MustGetMock().SetMockData(context.Background(), *tt.args.mockData) + err := SetMockData(context.Background(), tt.fields.clientset, tt.fields.namespace, *tt.args.mockData) if (err != nil) != tt.wantErr { - t.Errorf("Mock.SetMockData() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("SetMockData() error = %v, wantErr %v", err, tt.wantErr) return } secret, err := tt.fields.clientset.CoreV1().Secrets(tt.fields.namespace).Get(context.Background(), replicatedSecretName, metav1.GetOptions{}) require.NoError(t, err) - var got MockData - err = yaml.Unmarshal(secret.Data[replicatedMockDataKey], &got) + var got integrationtypes.MockData + err = yaml.Unmarshal(secret.Data[replicatedIntegrationMockDataKey], &got) require.NoError(t, err) if !reflect.DeepEqual(tt.want, &got) { - t.Errorf("Mock.SetMockData() \n\n%s", fmtJSONDiff(got, tt.want)) + t.Errorf("SetMockData() \n\n%s", fmtJSONDiff(got, tt.want)) } }) } } -func GetTestMockData() (*MockData, error) { - var testMockData *MockData +func GetTestMockData() (*integrationtypes.MockData, error) { + var testMockData *integrationtypes.MockData err := yaml.Unmarshal([]byte(testMockDataYAML), &testMockData) if err != nil { return nil, err diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go new file mode 100644 index 00000000..6c026674 --- /dev/null +++ b/pkg/integration/types/types.go @@ -0,0 +1,18 @@ +package types + +type MockData struct { + HelmChartURL string `json:"helmChartURL,omitempty" yaml:"helmChartURL,omitempty"` + CurrentRelease *MockRelease `json:"currentRelease,omitempty" yaml:"currentRelease,omitempty"` + DeployedReleases []MockRelease `json:"deployedReleases,omitempty" yaml:"deployedReleases,omitempty"` + AvailableReleases []MockRelease `json:"availableReleases,omitempty" yaml:"availableReleases,omitempty"` +} + +type MockRelease struct { + VersionLabel string `json:"versionLabel" yaml:"versionLabel"` + ReleaseNotes string `json:"releaseNotes" yaml:"releaseNotes"` + CreatedAt string `json:"createdAt" yaml:"createdAt"` + DeployedAt string `json:"deployedAt" yaml:"deployedAt"` + HelmReleaseName string `json:"helmReleaseName" yaml:"helmReleaseName"` + HelmReleaseRevision int `json:"helmReleaseRevision" yaml:"helmReleaseRevision"` + HelmReleaseNamespace string `json:"helmReleaseNamespace" yaml:"helmReleaseNamespace"` +} diff --git a/pkg/mock/mock.go b/pkg/mock/mock.go deleted file mode 100644 index b8196c39..00000000 --- a/pkg/mock/mock.go +++ /dev/null @@ -1,228 +0,0 @@ -package mock - -import ( - "context" - _ "embed" - "strconv" - "sync" - - "github.com/pkg/errors" - kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1" - "gopkg.in/yaml.v2" - corev1 "k8s.io/api/core/v1" - kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -const ( - replicatedSecretName = "replicated" - replicatedMockDataKey = "REPLICATED_MOCK_DATA" - replicatedMockEnabledKey = "REPLICATED_MOCK_ENABLED" - - CurrentReleaseMockKey = "currentRelease" - DeployedReleasesMockKey = "deployedReleases" - AvailableReleasesMockKey = "availableReleases" -) - -var ( - mock *Mock - replicatedSecretLock = sync.Mutex{} - - //go:embed default_mock_data.yaml - defaultMockDataYAML []byte -) - -type Mock struct { - clientset kubernetes.Interface - namespace string -} - -func InitMock(clientset kubernetes.Interface, namespace string) { - mock = &Mock{ - clientset: clientset, - namespace: namespace, - } -} - -func MustGetMock() *Mock { - if mock == nil { - panic("mock not initialized") - } - return mock -} - -type MockData struct { - HelmChartURL string `json:"helmChartURL,omitempty" yaml:"helmChartURL,omitempty"` - CurrentRelease *MockRelease `json:"currentRelease,omitempty" yaml:"currentRelease,omitempty"` - DeployedReleases []MockRelease `json:"deployedReleases,omitempty" yaml:"deployedReleases,omitempty"` - AvailableReleases []MockRelease `json:"availableReleases,omitempty" yaml:"availableReleases,omitempty"` -} - -type MockRelease struct { - VersionLabel string `json:"versionLabel" yaml:"versionLabel"` - ReleaseNotes string `json:"releaseNotes" yaml:"releaseNotes"` - CreatedAt string `json:"createdAt" yaml:"createdAt"` - DeployedAt string `json:"deployedAt" yaml:"deployedAt"` - HelmReleaseName string `json:"helmReleaseName" yaml:"helmReleaseName"` - HelmReleaseRevision int `json:"helmReleaseRevision" yaml:"helmReleaseRevision"` - HelmReleaseNamespace string `json:"helmReleaseNamespace" yaml:"helmReleaseNamespace"` -} - -func (m *Mock) IsMockEnabled(ctx context.Context, license *kotsv1beta1.License) (bool, error) { - if license == nil || license.Spec.LicenseType != "dev" { - return false, nil - } - - replicatedSecretLock.Lock() - defer replicatedSecretLock.Unlock() - - secret, err := m.clientset.CoreV1().Secrets(m.namespace).Get(ctx, replicatedSecretName, metav1.GetOptions{}) - if err != nil { - if kuberneteserrors.IsNotFound(err) { - return false, nil - } - return false, errors.Wrap(err, "failed to get replicated secret") - } - - v, ok := secret.Data[replicatedMockEnabledKey] - if !ok { - return true, nil - } - - enabled, _ := strconv.ParseBool(string(v)) - return enabled, nil -} - -func (m *Mock) GetHelmChartURL(ctx context.Context) (string, error) { - mockData, err := m.GetMockData(ctx) - if err != nil { - return "", errors.Wrap(err, "failed to get mock data") - } else if mockData == nil { - return "", nil - } - - return mockData.HelmChartURL, nil -} - -func (m *Mock) GetCurrentRelease(ctx context.Context) (*MockRelease, error) { - mockData, err := m.GetMockData(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to get mock data") - } else if mockData == nil { - return nil, nil - } - - return mockData.CurrentRelease, nil -} - -func (m *Mock) GetAvailableReleases(ctx context.Context) ([]MockRelease, error) { - mockData, err := m.GetMockData(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to get mock data") - } else if mockData == nil { - return nil, nil - } - - return mockData.AvailableReleases, nil -} - -func (m *Mock) GetDeployedReleases(ctx context.Context) ([]MockRelease, error) { - mockData, err := m.GetMockData(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to get mock data") - } else if mockData == nil { - return nil, nil - } - - return mockData.DeployedReleases, nil -} - -func (m *Mock) SetMockData(ctx context.Context, mockData MockData) error { - replicatedSecretLock.Lock() - defer replicatedSecretLock.Unlock() - - b, err := yaml.Marshal(mockData) - if err != nil { - return errors.Wrap(err, "failed to marshal mock data") - } - - secret, err := m.clientset.CoreV1().Secrets(m.namespace).Get(ctx, replicatedSecretName, metav1.GetOptions{}) - if err != nil { - if kuberneteserrors.IsNotFound(err) { - data := map[string][]byte{ - replicatedMockDataKey: b, - } - err = m.createReplicatedSecret(ctx, data) - if err != nil { - return errors.Wrap(err, "failed to create secret replicated") - } - return nil - } - - return errors.Wrap(err, "failed to get replicated secret") - } - - if secret.Data == nil { - secret.Data = map[string][]byte{} - } - - secret.Data[replicatedMockDataKey] = b - _, err = m.clientset.CoreV1().Secrets(m.namespace).Update(ctx, secret, metav1.UpdateOptions{}) - if err != nil { - return errors.Wrap(err, "failed to update replicated secret") - } - - return nil -} - -func (m *Mock) GetMockData(ctx context.Context) (*MockData, error) { - replicatedSecretLock.Lock() - defer replicatedSecretLock.Unlock() - - secret, err := m.clientset.CoreV1().Secrets(m.namespace).Get(ctx, replicatedSecretName, metav1.GetOptions{}) - if err != nil && !kuberneteserrors.IsNotFound(err) { - return nil, errors.Wrap(err, "failed to get replicated secret") - } - if err == nil { - b := secret.Data[replicatedMockDataKey] - if len(b) != 0 { - var mockData MockData - if err := yaml.Unmarshal(b, &mockData); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal mock data") - } - return &mockData, nil - } - } - - return GetDefaultMockData(ctx) -} - -func GetDefaultMockData(ctx context.Context) (*MockData, error) { - var mockData MockData - if err := yaml.Unmarshal(defaultMockDataYAML, &mockData); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal default mock data") - } - return &mockData, nil -} - -func (m *Mock) createReplicatedSecret(ctx context.Context, data map[string][]byte) error { - secret := &corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: replicatedSecretName, - Namespace: m.namespace, - }, - Data: data, - } - - _, err := m.clientset.CoreV1().Secrets(m.namespace).Create(ctx, secret, metav1.CreateOptions{}) - if err != nil { - return errors.Wrap(err, "failed to create secret replicated") - } - - return nil -}