diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ffb2f25..0a09759 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,7 @@ defaults: env: TERM: xterm + GO_VERSION: 1.22.0 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -28,7 +29,7 @@ jobs: uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: cache: true - go-version-file: 'go.mod' + go-version: ${{ env.GO_VERSION }} - name: ci/setup-node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: diff --git a/Makefile b/Makefile index abe9365..4cc055a 100644 --- a/Makefile +++ b/Makefile @@ -22,10 +22,10 @@ build_desktop_desktop: build_desktop_macos .PHONY: lint-server lint-server: - @echo Running staticcheck + @echo Running lint @echo $(GOBIN) - GOBIN=$(GOBIN) $(GO) install honnef.co/go/tools/cmd/staticcheck@latest - $(GOBIN)/staticcheck ./... + GOBIN=$(GOBIN) $(GO) install golang.org/x/lint/golint + $(GOBIN)/golint -set_exit_status $(./...) @echo lint success .PHONY: govet @@ -47,20 +47,4 @@ lint-webapp: node_modules cd webapp; npm run lint .PHONY: check-style -check-style: lint-server govet lint-webapp - -.PHONY: build-webapp -build-webapp: node_modules - @echo Building webapp for production - cd webapp; npm run build - -.PHONY: build-server-embedded -build-server-embedded: build-webapp - @echo Building server with embedded frontend - @echo Copying webapp build files to static/build - rm -rf static/build - mkdir -p static - cp -r webapp/build static/ - go build -o build/mcnb-server ./cmd/mcnb - @echo Cleaning up copied files - rm -rf static/build \ No newline at end of file +check-style: lint-server govet lint-webapp \ No newline at end of file diff --git a/api/api.go b/api/api.go index 94e1870..25d2050 100644 --- a/api/api.go +++ b/api/api.go @@ -8,5 +8,4 @@ func Register(rootRouter *mux.Router, c *Context) { apiRouter := rootRouter.PathPrefix("/api/v1").Subrouter() initBootstrapper(apiRouter, c) initState(apiRouter, c) - initTelemetry(apiRouter, c) } diff --git a/api/bootstrapper.go b/api/bootstrapper.go index 65e7b85..8859774 100644 --- a/api/bootstrapper.go +++ b/api/bootstrapper.go @@ -77,6 +77,9 @@ func initBootstrapper(apiRouter *mux.Router, context *Context) { } func handleSetCredentials(c *Context, w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + cloudProvider := vars["cloudProvider"] + var credentials model.Credentials json.NewDecoder(r.Body).Decode(&credentials) @@ -99,7 +102,8 @@ func handleSetCredentials(c *Context, w http.ResponseWriter, r *http.Request) { return } - err = UpdateStateCredentials(c.BootstrapperState, &credentials) + // Update both credentials and provider in state + err = UpdateStateCredentialsAndProvider(c.BootstrapperState, &credentials, cloudProvider) if err != nil { logger.FromContext(c.Ctx).WithError(err).Error("Failed to update state credentials - settings will not be persisted") } @@ -199,6 +203,12 @@ func handleGetCluster(c *Context, w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) } + // Update cluster name in state when accessing a cluster + err = UpdateStateClusterName(c.BootstrapperState, clusterName) + if err != nil { + logger.FromContext(c.Ctx).WithError(err).Error("Failed to update cluster name in state") + } + json.NewEncoder(w).Encode(result) } @@ -431,7 +441,7 @@ func handleDeployNginxOperator(c *Context, w http.ResponseWriter, r *http.Reques annotations: service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp" service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "https" - service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:us-east-1:926412419614:certificate/e13f9426-e452-4670-9f6a-f56b3f346bf1` + service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:us-east-1:110643744285:certificate/8fcc5250-8a60-4ab8-8337-7491fb447906` chartSpec := helmclient.ChartSpec{ ReleaseName: "ingress-nginx", @@ -754,6 +764,7 @@ func handlePatchMattermostInstallation(c *Context, w http.ResponseWriter, r *htt logger.FromContext(c.Ctx).Infof("Patch request: %+v", patchRequest.FilestorePatch) if !patchRequest.IsValid() { + logger.FromContext(c.Ctx).Errorf("Invalid patch request - version validation failed for version: %s", patchRequest.Version) w.WriteHeader(http.StatusBadRequest) return } @@ -899,6 +910,12 @@ func handlePatchMattermostInstallation(c *Context, w http.ResponseWriter, r *htt database.External.Secret = updatedSecret.ObjectMeta.Name } + // Update environment variables if provided + if len(patchRequest.MattermostEnv) > 0 { + logger.FromContext(c.Ctx).Infof("Updating environment variables, count: %d", len(patchRequest.MattermostEnv)) + installation.Spec.MattermostEnv = patchRequest.MattermostEnv + } + installation.Spec.Version = patchRequest.Version installation.Spec.Image = patchRequest.Image installation.Spec.FileStore = MMFilestore @@ -1009,6 +1026,7 @@ func handleCreateMattermostInstallation(c *Context, w http.ResponseWriter, r *ht var writer string var reader string + var databaseSecretName string if create.DBConnectionOption == model.DatabaseOptionCreateForMe { dbCluster := &cnpgv1.Cluster{ @@ -1066,29 +1084,38 @@ func handleCreateMattermostInstallation(c *Context, w http.ResponseWriter, r *ht writer = strings.Replace(initial, "postgresql:", "postgres:", 1) // Replace once reader = strings.Replace(writer, fmt.Sprintf("%s-rw:", secretName), fmt.Sprintf("%s-ro:", secretName), 1) } else if create.DBConnectionOption == model.DatabaseOptionExisting { - writer = create.ExistingDBConnection.ConnectionString - reader = create.ExistingDBConnection.ConnectionString + if create.ExistingDBSecretName != "" { + databaseSecretName = create.ExistingDBSecretName + } else { + writer = create.ExistingDBConnection.ConnectionString + reader = create.ExistingDBConnection.ConnectionString + } } - databaseSecret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: model.SecretNameDatabase, - Namespace: namespaceName, - }, - Type: v1.SecretTypeOpaque, - StringData: map[string]string{ - "DB_CONNECTION_CHECK_URL": writer, - "DB_CONNECTION_STRING": writer, - "MM_SQLSETTINGS_DATASOURCEREPLICAS": reader, // Assuming read replicas for now - }, - } + if databaseSecretName == "" { + databaseSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: model.SecretNameDatabase, + Namespace: namespaceName, + }, + Type: v1.SecretTypeOpaque, + StringData: map[string]string{ + "DB_CONNECTION_CHECK_URL": writer, + "DB_CONNECTION_STRING": writer, + "MM_SQLSETTINGS_DATASOURCEREPLICAS": reader, // Assuming read replicas for now + "MM_CONFIG": writer, + }, + } - // Create the database secret - _, err = kubeClient.Clientset.CoreV1().Secrets(namespaceName).Create(context.TODO(), databaseSecret, metav1.CreateOptions{}) - if err != nil { - logger.FromContext(c.Ctx).Errorf("Error creating database secret:", err) - w.WriteHeader(http.StatusInternalServerError) - return + // Create the database secret + _, err = kubeClient.Clientset.CoreV1().Secrets(namespaceName).Create(context.TODO(), databaseSecret, metav1.CreateOptions{}) + if err != nil { + logger.FromContext(c.Ctx).Errorf("Error creating database secret:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + databaseSecretName = databaseSecret.ObjectMeta.Name } // License Secret @@ -1114,6 +1141,9 @@ func handleCreateMattermostInstallation(c *Context, w http.ResponseWriter, r *ht } filestore := create.GetMMOperatorFilestore(namespaceName, filestoreSecret) + if filestore.External != nil && filestore.External.Secret == "" && create.FilestoreSecretName != "" { + filestore.External.Secret = create.FilestoreSecretName + } mattermostCRD := &mmv1beta1.Mattermost{ ObjectMeta: metav1.ObjectMeta{ @@ -1133,7 +1163,12 @@ func handleCreateMattermostInstallation(c *Context, w http.ResponseWriter, r *ht }, Database: mmv1beta1.Database{ External: &mmv1beta1.ExternalDatabase{ - Secret: model.SecretNameDatabase, + Secret: func() string { + if databaseSecretName != "" { + return databaseSecretName + } + return model.SecretNameDatabase + }(), }, }, FileStore: filestore, @@ -1143,6 +1178,17 @@ func handleCreateMattermostInstallation(c *Context, w http.ResponseWriter, r *ht {Name: model.MMENVLicense, ValueFrom: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{Key: "license", LocalObjectReference: v1.LocalObjectReference{Name: licenseSecret.ObjectMeta.Name}, Optional: aws.Bool(true)}, // Add comma to separate items }}, + {Name: "MM_CONFIG", ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + Key: "MM_CONFIG", + LocalObjectReference: v1.LocalObjectReference{Name: func() string { + if databaseSecretName != "" { + return databaseSecretName + } + return model.SecretNameDatabase + }()}, + }, + }}, }, PodTemplate: &mmv1beta1.PodTemplate{ SecurityContext: &v1.PodSecurityContext{ @@ -1219,11 +1265,11 @@ func handleDeployMattermostOperator(c *Context, w http.ResponseWriter, r *http.R } chartSpec := helmclient.ChartSpec{ - ReleaseName: "mattermost-operator", - ChartName: "mattermost/mattermost-operator", - Namespace: "mattermost-operator", - UpgradeCRDs: true, - Version: "v1.22.0", + ReleaseName: "mattermost-operator", + ChartName: "mattermost/mattermost-operator", + Namespace: "mattermost-operator", + UpgradeCRDs: true, + // Version: "1.25.2", Wait: true, Timeout: 300 * time.Second, CreateNamespace: true, diff --git a/api/context.go b/api/context.go index d12e830..55dda74 100644 --- a/api/context.go +++ b/api/context.go @@ -9,7 +9,6 @@ import ( "github.com/mattermost/mattermost-cloudnative-bootstrapper/internal/logger" "github.com/mattermost/mattermost-cloudnative-bootstrapper/model" "github.com/mattermost/mattermost-cloudnative-bootstrapper/providers" - "github.com/mattermost/mattermost-cloudnative-bootstrapper/telemetry" ) type BootstrapperState struct { @@ -30,7 +29,6 @@ type Context struct { CloudProviderName string CloudProvider providers.CloudProvider BootstrapperState BootstrapperState - TelemetryProvider *telemetry.TelemetryProvider } func NewContext(ctx context.Context, statePath string, telemetryDisabled bool) (*Context, error) { @@ -74,18 +72,11 @@ func NewContext(ctx context.Context, statePath string, telemetryDisabled bool) ( } } - // Initialize the telemetry provider - telemetryProvider, err := telemetry.NewTelemetryProvider(state.Telemetry.TelemetryID, state.Telemetry.TelemetryDisabled) - if err != nil { - return nil, err - } - return &Context{ Ctx: ctx, BootstrapperState: state, // TODO: this is redundant, use the state.Provider instead everywhere CloudProviderName: state.Provider, - TelemetryProvider: telemetryProvider, }, nil } @@ -97,7 +88,6 @@ func (c *Context) Clone() *Context { CloudProviderName: c.CloudProviderName, CloudProvider: c.CloudProvider, BootstrapperState: c.BootstrapperState, - TelemetryProvider: c.TelemetryProvider, } } diff --git a/api/response_writer_wrapper.go b/api/response_writer_wrapper.go index 08dcea3..368b80b 100644 --- a/api/response_writer_wrapper.go +++ b/api/response_writer_wrapper.go @@ -52,7 +52,7 @@ func (rw *ResponseWriterWrapper) Write(data []byte) (int, error) { // Hijack calls the underlying writer's Hijack output. func (rw *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { if rw.hijacker == nil { - return nil, nil, errors.New("hijacker interface not supported by the wrapped ResponseWriter") + return nil, nil, errors.New("Hijacker interface not supported by the wrapped ResponseWriter") } return rw.hijacker.Hijack() } diff --git a/api/state.go b/api/state.go index 002e10e..dc78b13 100644 --- a/api/state.go +++ b/api/state.go @@ -17,6 +17,7 @@ func initState(apiRouter *mux.Router, context *Context) { stateRouter := apiRouter.PathPrefix("/state").Subrouter() stateRouter.Handle("/hydrate", addContext(handleHydrateState)).Methods("GET") + stateRouter.Handle("/check", addContext(handleCheckState)).Methods("GET") stateRouter.Handle("", addContext(handlePatchState)).Methods("PATCH") } @@ -28,6 +29,42 @@ func handleHydrateState(c *Context, w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(state) } +// SessionInfo represents a summary of an existing session for UI display +type SessionInfo struct { + Provider string `json:"provider"` + ClusterName string `json:"clusterName"` + HasState bool `json:"hasState"` +} + +func handleCheckState(c *Context, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Check if a state file exists + exists, err := CheckStateExists(c.BootstrapperState.StateFilePath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if !exists { + // No state exists + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(SessionInfo{HasState: false}) + return + } + + // State exists, return session info + state := c.BootstrapperState + sessionInfo := SessionInfo{ + Provider: state.Provider, + ClusterName: state.ClusterName, + HasState: true, + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(sessionInfo) +} + func handlePatchState(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -156,3 +193,38 @@ func UpdateStateCredentials(existingState BootstrapperState, credentials *model. return nil } + +// UpdateStateCredentialsAndProvider updates credentials and provider in state +func UpdateStateCredentialsAndProvider(existingState BootstrapperState, credentials *model.Credentials, provider string) error { + state, err := GetState(existingState.StateFilePath) + if err != nil { + return err + } + + state.Credentials = credentials + state.Provider = provider + + err = SetState(existingState.StateFilePath, state) + if err != nil { + return err + } + + return nil +} + +// UpdateStateClusterName updates the cluster name in state +func UpdateStateClusterName(existingState BootstrapperState, clusterName string) error { + state, err := GetState(existingState.StateFilePath) + if err != nil { + return err + } + + state.ClusterName = clusterName + + err = SetState(existingState.StateFilePath, state) + if err != nil { + return err + } + + return nil +} diff --git a/api/telemetry.go b/api/telemetry.go deleted file mode 100644 index 0f3e1cd..0000000 --- a/api/telemetry.go +++ /dev/null @@ -1,81 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost-cloudnative-bootstrapper/internal/logger" - "github.com/mattermost/mattermost-cloudnative-bootstrapper/telemetry" -) - -func initTelemetry(apiRouter *mux.Router, context *Context) { - addContext := func(handler contextHandlerFunc) *contextHandler { - return newContextHandler(context, handler) - } - - telemetryRouter := apiRouter.PathPrefix("/telemetry").Subrouter() - telemetryRouter.Handle("/track", addContext(handleTrack)).Methods("POST") - telemetryRouter.Handle("/identify", addContext(handleIdentify)).Methods("POST") - telemetryRouter.Handle("/page", addContext(handlePage)).Methods("POST") -} - -func handleTrack(c *Context, w http.ResponseWriter, r *http.Request) { - var track telemetry.Track - - err := json.NewDecoder(r.Body).Decode(&track) - if err != nil { - logger.FromContext(c.Ctx).WithError(err).Error("Failed to decode telemetry track") - w.WriteHeader(http.StatusBadRequest) - return - } - - err = c.TelemetryProvider.Track(&track) - if err != nil { - logger.FromContext(c.Ctx).WithError(err).Error("Failed to track telemetry track") - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) -} - -func handleIdentify(c *Context, w http.ResponseWriter, r *http.Request) { - var identify telemetry.Identify - - err := json.NewDecoder(r.Body).Decode(&identify) - if err != nil { - logger.FromContext(c.Ctx).WithError(err).Error("Failed to decode telemetry identify") - w.WriteHeader(http.StatusBadRequest) - return - } - - err = c.TelemetryProvider.Identify(&identify) - if err != nil { - logger.FromContext(c.Ctx).WithError(err).Error("Failed to identify telemetry user") - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) -} - -func handlePage(c *Context, w http.ResponseWriter, r *http.Request) { - var page telemetry.Page - - err := json.NewDecoder(r.Body).Decode(&page) - if err != nil { - logger.FromContext(c.Ctx).WithError(err).Error("Failed to decode telemetry page") - w.WriteHeader(http.StatusBadRequest) - return - } - - err = c.TelemetryProvider.Page(&page) - if err != nil { - logger.FromContext(c.Ctx).WithError(err).Error("Failed to track telemetry page") - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) -} diff --git a/cmd/mcnb/server.go b/cmd/mcnb/server.go index 22833b0..5e68760 100644 --- a/cmd/mcnb/server.go +++ b/cmd/mcnb/server.go @@ -2,23 +2,22 @@ package main import ( "context" - "io" "log" "net/http" "os" "os/signal" - "strings" "syscall" "time" "github.com/gorilla/mux" "github.com/mattermost/mattermost-cloudnative-bootstrapper/api" "github.com/mattermost/mattermost-cloudnative-bootstrapper/internal/logger" - "github.com/mattermost/mattermost-cloudnative-bootstrapper/static" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +const defaultLocalServerAPI = "http://localhost:8070" + var serverCmd = &cobra.Command{ Use: "server", Short: "Run the Mattermost CloudNative Bootstrapper server", @@ -40,47 +39,6 @@ var serverCmd = &cobra.Command{ api.Register(r, apiContext) - // Serve embedded static files if available (server mode) - if static.IsEmbedded() { - logger.FromContext(ctx).Info("Serving embedded frontend files") - staticFileSystem, err := static.GetEmbeddedFileSystem() - if err != nil { - logger.FromContext(ctx).WithError(err).Error("Failed to get embedded file system") - } else { - // Create a custom handler for SPA routing - spaHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // Skip API routes - let them return 404 naturally - if strings.HasPrefix(req.URL.Path, "/api/v1") { - http.NotFound(w, req) - return - } - - // Try to serve the requested file first - if file, err := staticFileSystem.Open(req.URL.Path[1:]); err == nil { - defer file.Close() - http.ServeContent(w, req, req.URL.Path, time.Time{}, file.(io.ReadSeeker)) - return - } - - // If file not found and it's not an API route, serve index.html for SPA routing - indexFile, err := staticFileSystem.Open("index.html") - if err != nil { - http.NotFound(w, req) - return - } - defer indexFile.Close() - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - http.ServeContent(w, req, "index.html", time.Time{}, indexFile.(io.ReadSeeker)) - }) - - // Serve everything through our custom handler - r.PathPrefix("/").Handler(spaHandler) - } - } else { - logger.FromContext(ctx).Info("No embedded frontend files found - running in API-only mode") - } - srv := &http.Server{ Addr: ":8070", Handler: r, diff --git a/go.mod b/go.mod index 5725e47..0ad659f 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/mattermost/mattermost-cloudnative-bootstrapper -go 1.24 +go 1.22.0 -toolchain go1.24.3 +toolchain go1.22.3 require ( github.com/aws/aws-sdk-go v1.50.26 @@ -13,8 +13,6 @@ require ( github.com/mattermost/mattermost-cloud v0.81.2 github.com/mattermost/mattermost-operator v1.21.0-rc.2 github.com/mittwald/go-helm-client v0.12.8 - github.com/pborman/uuid v1.2.1 - github.com/rudderlabs/analytics-go/v4 v4.2.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 @@ -112,6 +110,7 @@ require ( github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -123,6 +122,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/rubenv/sql-migrate v1.6.1 // indirect + github.com/rudderlabs/analytics-go v3.3.3+incompatible // indirect + github.com/rudderlabs/analytics-go/v4 v4.2.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/backo-go v1.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect @@ -135,6 +136,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect @@ -144,6 +146,7 @@ require ( go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/go.sum b/go.sum index ce0933b..54f3d00 100644 --- a/go.sum +++ b/go.sum @@ -410,6 +410,8 @@ github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos= github.com/rubenv/sql-migrate v1.6.1/go.mod h1:tPzespupJS0jacLfhbwto/UjSX+8h2FdWB7ar+QlHa0= +github.com/rudderlabs/analytics-go v3.3.3+incompatible h1:OG0XlKoXfr539e2t1dXtTB+Gr89uFW+OUNQBVhHIIBY= +github.com/rudderlabs/analytics-go v3.3.3+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30= github.com/rudderlabs/analytics-go/v4 v4.2.1 h1:J1fnTDXC8jNwytgOgnTgW5/IYafuvaCODE4UsNC8siU= github.com/rudderlabs/analytics-go/v4 v4.2.1/go.mod h1:/kXZkGO7S0of698Z62p8Y6KPH1nFaok32di9WPZMiE4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -446,6 +448,7 @@ github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -468,6 +471,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -501,17 +506,23 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -521,6 +532,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= @@ -531,6 +544,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -548,11 +563,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -560,15 +579,20 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/model/cluster.go b/model/cluster.go index 6176897..9fdc237 100644 --- a/model/cluster.go +++ b/model/cluster.go @@ -56,6 +56,7 @@ type ClusterNodegroup struct { type Credentials struct { AccessKeyID string `json:"accessKeyID"` SecretAccessKey string `json:"accessKeySecret"` + SessionToken string `json:"sessionToken"` Region string `json:"region"` Kubecfg string `json:"kubeconfig"` KubecfgType string `json:"kubeconfigType"` diff --git a/model/mattermost.go b/model/mattermost.go index 36f5f83..6600173 100644 --- a/model/mattermost.go +++ b/model/mattermost.go @@ -42,8 +42,10 @@ type CreateMattermostWorkspaceRequest struct { Version string `json:"version"` DBConnectionOption string `json:"dbConnectionOption"` ExistingDBConnection *ExistingDBConnection `json:"existingDatabaseConfig"` + ExistingDBSecretName string `json:"existingDatabaseSecretName"` FilestoreOption string `json:"filestoreOption"` S3Filestore *S3Filestore `json:"s3FilestoreConfig"` + FilestoreSecretName string `json:"filestoreSecretName"` LocalFileStore *LocalFileStore `json:"localFilestoreConfig"` LocalExternalFileStore *LocalExternalFileStore `json:"localExternalFilestoreConfig"` } @@ -122,11 +124,19 @@ func (is *InstallationSecrets) ToInstallationSecretsResponse() (*InstallationSec } func (le *LocalExternalFileStore) IsValid() bool { - return le.VolumeClaimName != "" + if le.VolumeClaimName == "" { + return false + } + + return true } func (l *LocalFileStore) IsValid() bool { - return l.StorageSize != "" + if l.StorageSize == "" { + return false + } + + return true } func (e *ExistingDBConnection) IsValid() bool { @@ -162,6 +172,9 @@ func (s *S3Filestore) IsValid() bool { } func (c *CreateMattermostWorkspaceRequest) GetMMOperatorFilestoreSecret(namespaceName string) *v1.Secret { + if c.FilestoreSecretName != "" { + return nil + } if c.FilestoreOption == FilestoreOptionExistingS3 { return &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -183,10 +196,15 @@ func (c *CreateMattermostWorkspaceRequest) GetMMOperatorFilestoreSecret(namespac func (c *CreateMattermostWorkspaceRequest) GetMMOperatorFilestore(namespaceName string, secret *v1.Secret) mmv1beta1.FileStore { filestore := mmv1beta1.FileStore{} if c.FilestoreOption == FilestoreOptionExistingS3 { - filestore.External = &mmv1beta1.ExternalFileStore{ - URL: c.S3Filestore.BucketURL, - Secret: secret.Name, - Bucket: c.S3Filestore.BucketName, + filestore.External = &mmv1beta1.ExternalFileStore{} + if c.S3Filestore != nil { + filestore.External.URL = c.S3Filestore.BucketURL + filestore.External.Bucket = c.S3Filestore.BucketName + } + if secret != nil { + filestore.External.Secret = secret.Name + } else if c.FilestoreSecretName != "" { + filestore.External.Secret = c.FilestoreSecretName } } else if c.FilestoreOption == FilestoreOptionInClusterLocal { filestore.Local = &mmv1beta1.LocalFileStore{ @@ -214,6 +232,7 @@ type PatchMattermostWorkspaceRequest struct { Endpoint string `json:"endpoint"` FilestorePatch *PatchMattermostFilestoreRequest `json:"fileStorePatch"` DatabasePatch *PatchMattermostDatabaseRequest `json:"databasePatch"` + MattermostEnv []v1.EnvVar `json:"mattermostEnv"` } type PatchMattermostFilestoreRequest struct { @@ -262,8 +281,9 @@ func isValidReleaseBranchTag(tag string) bool { // isValidSemanticVersion checks if the provided string is a valid semantic version. func isValidSemanticVersion(version string) bool { - // Regular expression pattern for semantic versioning (e.g., 1.0.0) - pattern := `^\d+\.\d+\.\d+$` + // Regular expression pattern for semantic versioning (e.g., 1.0.0 or 10.12) + // Supports both X.Y.Z and X.Y formats + pattern := `^\d+\.\d+(\.\d+)?$` match, err := regexp.MatchString(pattern, version) return err == nil && match } @@ -282,7 +302,7 @@ func (c *CreateMattermostWorkspaceRequest) IsValid() bool { return false } - if c.FilestoreOption == FilestoreOptionExistingS3 && !c.S3Filestore.IsValid() { + if c.FilestoreOption == FilestoreOptionExistingS3 && c.FilestoreSecretName == "" && (c.S3Filestore == nil || !c.S3Filestore.IsValid()) { return false } @@ -298,7 +318,7 @@ func (c *CreateMattermostWorkspaceRequest) IsValid() bool { return false } - if c.DBConnectionOption == DatabaseOptionExisting && !c.ExistingDBConnection.IsValid() { + if c.DBConnectionOption == DatabaseOptionExisting && c.ExistingDBSecretName == "" && (c.ExistingDBConnection == nil || !c.ExistingDBConnection.IsValid()) { return false } diff --git a/providers/aws.go b/providers/aws.go index 779491c..96303d9 100644 --- a/providers/aws.go +++ b/providers/aws.go @@ -52,6 +52,7 @@ func GetAWSProvider(credentials *model.Credentials) *AWSProvider { credentials = &model.Credentials{ AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), + SessionToken: os.Getenv("AWS_SESSION_TOKEN"), } } awsProviderInstance = &AWSProvider{ @@ -69,6 +70,7 @@ func (a *AWSProvider) GetAWSCredentials() model.Credentials { return model.Credentials{ AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), + SessionToken: os.Getenv("AWS_SESSION_TOKEN"), } } return *a.Credentials @@ -102,7 +104,7 @@ func (a *AWSProvider) NewEKSClient(region ...string) *EKSClient { } sess, err := session.NewSession(&aws.Config{ - Credentials: credentials.NewStaticCredentials(awsCredentials.AccessKeyID, awsCredentials.SecretAccessKey, ""), + Credentials: credentials.NewStaticCredentials(awsCredentials.AccessKeyID, awsCredentials.SecretAccessKey, awsCredentials.SessionToken), Region: aws.String(defaultRegion), // Specify the appropriate AWS region }) if err != nil { @@ -136,7 +138,7 @@ func (a *AWSProvider) SetRegion(c context.Context, region string) error { func (a *AWSProvider) ValidateCredentials(c context.Context, creds *model.Credentials) (bool, error) { // Create a new session with the provided credentials sess, err := session.NewSession(&aws.Config{ - Credentials: credentials.NewStaticCredentials(creds.AccessKeyID, creds.SecretAccessKey, ""), + Credentials: credentials.NewStaticCredentials(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken), Region: aws.String("us-east-1"), // Specify the appropriate AWS region // LogLevel: aws.LogLevel(aws.LogDebugWithHTTPBody), }) @@ -168,7 +170,7 @@ func (a *AWSProvider) ListRoles(c context.Context) ([]*model.SupportedRolesRespo sess, err := session.NewSession(&aws.Config{ Region: aws.String("us-east-1"), // Specify the appropriate AWS region - Credentials: credentials.NewStaticCredentials(awsCredentials.AccessKeyID, awsCredentials.SecretAccessKey, ""), + Credentials: credentials.NewStaticCredentials(awsCredentials.AccessKeyID, awsCredentials.SecretAccessKey, awsCredentials.SessionToken), }) if err != nil { @@ -181,8 +183,11 @@ func (a *AWSProvider) ListRoles(c context.Context) ([]*model.SupportedRolesRespo // List IAM roles input := &iam.ListRolesInput{} err = svc.ListRolesPages(input, func(page *iam.ListRolesOutput, lastPage bool) bool { - // TODO Filter down to only those roles that have permission? - eksSupportedRoles = append(eksSupportedRoles, page.Roles...) + for _, role := range page.Roles { + // TODO Filter down to only those roles that have permission? + + eksSupportedRoles = append(eksSupportedRoles, role) + } return !lastPage }) if err != nil { @@ -354,14 +359,25 @@ func (a *AWSProvider) GetKubeRestConfig(c context.Context, clusterName string) ( cluster := result.Cluster + // Create an STS client with our credentials + awsCredentials := a.GetAWSCredentials() + sess, err := session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials(awsCredentials.AccessKeyID, awsCredentials.SecretAccessKey, awsCredentials.SessionToken), + Region: aws.String(awsCredentials.Region), + }) + if err != nil { + return nil, fmt.Errorf("failed to create AWS session: %w", err) + } + + stsClient := sts.New(sess) + + // Generate token using the STS client directly gen, err := token.NewGenerator(true, false) if err != nil { return nil, err } - opts := &token.GetTokenOptions{ - ClusterID: aws.StringValue(cluster.Name), - } - tok, err := gen.GetWithOptions(opts) + + tok, err := gen.GetWithSTS(aws.StringValue(cluster.Name), stsClient) if err != nil { return nil, err } @@ -393,14 +409,25 @@ func (a *AWSProvider) GetKubeConfig(c context.Context, clusterName string) (clie cluster := result.Cluster + // Create an STS client with our credentials + awsCredentials := a.GetAWSCredentials() + sess, err := session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials(awsCredentials.AccessKeyID, awsCredentials.SecretAccessKey, awsCredentials.SessionToken), + Region: aws.String(awsCredentials.Region), + }) + if err != nil { + return nil, fmt.Errorf("failed to create AWS session: %w", err) + } + + stsClient := sts.New(sess) + + // Generate token using the STS client directly gen, err := token.NewGenerator(true, false) if err != nil { return nil, err } - opts := &token.GetTokenOptions{ - ClusterID: aws.StringValue(cluster.Name), - } - tok, err := gen.GetWithOptions(opts) + + tok, err := gen.GetWithSTS(aws.StringValue(cluster.Name), stsClient) if err != nil { return nil, err } @@ -500,7 +527,7 @@ func (a *AWSProvider) HelmFileStorePre(c context.Context, clusterName string, na // Install a chart release. if _, err := helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, nil); err != nil { - return errors.New("failed to install aws-ebs-csi-driver") + return errors.New("Failed to install aws-ebs-csi-driver") } return nil diff --git a/providers/custom.go b/providers/custom.go index 6c8c13a..163c39e 100644 --- a/providers/custom.go +++ b/providers/custom.go @@ -25,6 +25,7 @@ import ( type CustomKubeProvider struct { Credentials *model.Credentials credentialsLock *sync.Mutex + kubeClient *model.KubeClient } var customProviderInstance *CustomKubeProvider @@ -100,12 +101,12 @@ func (p *CustomKubeProvider) ValidateCredentials(c context.Context, creds *model kubeClient, err := p.KubeClient(c, "") if err != nil { - return false, fmt.Errorf("unable to instantiate KubeClient: %w", err) + return false, fmt.Errorf("Unable to instantiate KubeClient: %w", err) } _, err = kubeClient.Clientset.Discovery().ServerVersion() if err != nil { - return false, fmt.Errorf("unable to hit discovery endpoint for cluster: %w", err) + return false, fmt.Errorf("Unable to hit discovery endpoint for cluster: %w", err) } return true, nil diff --git a/static/build/.gitkeep b/static/build/.gitkeep deleted file mode 100644 index 48cdce8..0000000 --- a/static/build/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -placeholder diff --git a/static/embed.go b/static/embed.go deleted file mode 100644 index bc422a1..0000000 --- a/static/embed.go +++ /dev/null @@ -1,28 +0,0 @@ -package static - -import ( - "embed" - "io/fs" - "net/http" -) - -//go:embed all:build -var embeddedFiles embed.FS - -// GetEmbeddedFileSystem returns the embedded file system containing the webapp build files -// It returns the build subdirectory as the root to serve files from -func GetEmbeddedFileSystem() (http.FileSystem, error) { - buildDir, err := fs.Sub(embeddedFiles, "build") - if err != nil { - return nil, err - } - return http.FS(buildDir), nil -} - -// IsEmbedded returns true if the embedded files are available -// This can be used to check if the server was built with embedded assets -func IsEmbedded() bool { - // Try to read a common file that should exist in the build - _, err := embeddedFiles.Open("build/index.html") - return err == nil -} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go deleted file mode 100644 index 1b39d8e..0000000 --- a/telemetry/telemetry.go +++ /dev/null @@ -1,111 +0,0 @@ -package telemetry - -import ( - "os" - - rudder "github.com/rudderlabs/analytics-go/v4" -) - -type TelemetryProvider struct { - TelemetryDisabled bool `json:"telemetryDisabled"` - TelemetryID string `json:"telemetryID"` - Client rudder.Client -} - -type Track struct { - Event string `json:"event"` - Properties map[string]interface{} `json:"properties"` -} - -func (t *Track) ToRudderTrack(telemetryID string) rudder.Track { - return rudder.Track{ - AnonymousId: telemetryID, - Event: t.Event, - Properties: t.Properties, - } -} - -type Identify struct { - Traits map[string]interface{} `json:"traits"` - UserID string `json:"userId"` -} - -func (t *Identify) ToRudderIdentify(telemetryID string) rudder.Identify { - return rudder.Identify{ - AnonymousId: telemetryID, - Traits: t.Traits, - UserId: t.UserID, - } -} - -type Page struct { - Category string `json:"category"` - Name string `json:"name"` - Properties map[string]interface{} `json:"properties"` -} - -func (t *Page) ToRudderPage(telemetryID string) rudder.Page { - return rudder.Page{ - AnonymousId: telemetryID, - Name: t.Name, - Properties: t.Properties, - } -} - -const dataPlaneURL = "https://pdat.matterlytics.com" - -var writeKey = os.Getenv("RUDDER_WRITE_KEY") - -// NewTelemetryProvider creates a new TelemetryProvider -func NewTelemetryProvider(telemetryID string, telemetryDisabled bool) (*TelemetryProvider, error) { - client := rudder.New(writeKey, dataPlaneURL) - - provider := &TelemetryProvider{ - TelemetryID: telemetryID, - TelemetryDisabled: telemetryDisabled, - Client: client, - } - return provider, nil -} - -func (t *TelemetryProvider) Track(trackEvent *Track) error { - if t.TelemetryDisabled { - return nil - } - - track := trackEvent.ToRudderTrack(t.TelemetryID) - - return t.Client.Enqueue(track) -} - -func (t *TelemetryProvider) Page(pageEvent *Page) error { - if t.TelemetryDisabled { - return nil - } - - page := pageEvent.ToRudderPage(t.TelemetryID) - - return t.Client.Enqueue(page) -} - -func (t *TelemetryProvider) Identify(identifyEvent *Identify) error { - if t.TelemetryDisabled { - return nil - } - - identify := identifyEvent.ToRudderIdentify(t.TelemetryID) - - return t.Client.Enqueue(identify) -} - -func (t *TelemetryProvider) Flush() { - t.Client.Close() -} - -func (t *TelemetryProvider) IsDisabled() bool { - return t.TelemetryDisabled -} - -func (t *TelemetryProvider) GetTelemetryID() string { - return t.TelemetryID -} diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 75f80d7..f1e5a3f 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -17,7 +17,6 @@ import ExistingAWSPage from './pages/aws/choose_existing'; import CreateWorkspacePage from './pages/mattermost/create_workspace'; import InstallationDashboard from './pages/dashboard'; import RehydrateAndRedirect from './components/state'; -import useRudderPage from './hooks/useRudderPage'; const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; @@ -28,7 +27,6 @@ export default function JoyOrderDashboardTemplate() { const status = useScript(`https://unpkg.com/feather-icons`); const location = useLocation(); const [initialLoad, setInitialLoad] = React.useState(true); - const rudderPage = useRudderPage(); React.useEffect(() => { if (initialLoad) { @@ -39,7 +37,6 @@ export default function JoyOrderDashboardTemplate() { React.useEffect(() => { if (!initialLoad) { //Prevents infinite loop on load localStorage.setItem('lastVisitedPage', `${location.pathname}${location.search}`); - rudderPage(location.pathname.includes('dashboard') ? 'Dashboard' : 'Setup', location.pathname, {search: location.search, hash: location.hash}); } }, [location, initialLoad]); diff --git a/webapp/src/client/bootstrapperApi.ts b/webapp/src/client/bootstrapperApi.ts index de29eaf..76cbe0f 100644 --- a/webapp/src/client/bootstrapperApi.ts +++ b/webapp/src/client/bootstrapperApi.ts @@ -56,6 +56,9 @@ export const bootstrapperApi = createApi({ getState: builder.query({ query: () => '/state/hydrate', }), + checkExistingSession: builder.query<{ provider: string; clusterName: string; hasState: boolean }, void>({ + query: () => '/state/check', + }), setRegion: builder.mutation({ query: ({ region, cloudProvider }) => ({ url: `/${cloudProvider}/region`, @@ -166,6 +169,7 @@ export const { useGetNodegroupsQuery, useGetKubeConfigQuery, useGetStateQuery, + useCheckExistingSessionQuery, useSetRegionMutation, useWatchInstallationLogsQuery, useGetPodsForInstallationQuery, diff --git a/webapp/src/client/telemetryApi.ts b/webapp/src/client/telemetryApi.ts deleted file mode 100644 index a0ff4bf..0000000 --- a/webapp/src/client/telemetryApi.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; - -import { baseUrl } from './client'; - - - -export const telemetryApi = createApi({ - reducerPath: 'telemetryApi', - baseQuery: fetchBaseQuery({ baseUrl: `${baseUrl}/api/v1` }), - tagTypes: ['Telemetry'], - endpoints: (builder) => ({ - rudderPage: builder.mutation }>({ - query: ({ category, name, properties }) => ({ - url: `/telemetry/page`, - method: 'POST', - body: { category, name, properties }, - }), - }), - rudderTrack: builder.mutation }>({ - query: ({ event, properties }) => ({ - url: `/telemetry/track`, - method: 'POST', - body: { event, properties }, - }), - }), - rudderIdentify: builder.mutation }>({ - query: ({ userId, traits }) => ({ - url: `/telemetry/identify`, - method: 'POST', - body: { userId, traits }, - }), - }), - }) -}); - -export const { useRudderPageMutation, useRudderTrackMutation, useRudderIdentifyMutation } = telemetryApi; \ No newline at end of file diff --git a/webapp/src/components/dashboard/edit_installation_modal.scss b/webapp/src/components/dashboard/edit_installation_modal.scss index 9ec330b..5c37840 100644 --- a/webapp/src/components/dashboard/edit_installation_modal.scss +++ b/webapp/src/components/dashboard/edit_installation_modal.scss @@ -1,6 +1,20 @@ .edit-installation-modal { + .MuiModalDialog-root { + max-width: 800px !important; + width: 800px !important; + max-height: 85vh !important; + } + + .MuiDialogContent-root { + overflow: visible !important; + } + .edit-inputs { - overflow: scroll; + overflow-y: auto; + overflow-x: hidden; + max-height: calc(85vh - 150px); + padding-right: 10px; + label { display: block; margin-bottom: 5px; @@ -21,5 +35,43 @@ margin-bottom: 12px; } } + + .env-variables-section { + margin-top: 20px; + padding: 15px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #f9f9f9; + + .env-var-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + + h4 { + margin: 0; + font-size: 1.1em; + } + } + + .env-var-row { + display: grid; + grid-template-columns: 2fr 1.5fr 3fr 40px; + gap: 10px; + margin-bottom: 10px; + align-items: start; + + .MuiInput-root, .MuiSelect-root { + width: 100%; + } + + .secret-inputs { + display: flex; + flex-direction: column; + gap: 5px; + } + } + } } } \ No newline at end of file diff --git a/webapp/src/components/dashboard/edit_installation_modal.tsx b/webapp/src/components/dashboard/edit_installation_modal.tsx index e973129..67b9602 100644 --- a/webapp/src/components/dashboard/edit_installation_modal.tsx +++ b/webapp/src/components/dashboard/edit_installation_modal.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Mattermost, PatchMattermostWorkspaceRequest, LocalFileStore, FileStore, S3FileStore, ExistingDBConnection } from '../../types/Installation'; +import { Mattermost, PatchMattermostWorkspaceRequest, LocalFileStore, FileStore, S3FileStore, ExistingDBConnection, MattermostEnvItem } from '../../types/Installation'; import { Button, DialogContent, DialogTitle, Input, Modal, ModalClose, ModalDialog, Textarea } from '@mui/joy'; import './edit_installation_modal.scss'; import FilestoreConnection, { FilestoreConnectionDetails } from '../../pages/mattermost/filestore_connection'; @@ -7,6 +7,7 @@ import { useGetMattermostInstallationSecretsQuery } from '../../client/dashboard import { useSearchParams } from 'react-router-dom'; import DBConnection from '../../pages/mattermost/db_connection'; import { useGetInstalledHelmReleasesQuery } from '../../client/bootstrapperApi'; +import EnvVariablesManager from './env_variables_manager'; type EditInstallationModalProps = { installation?: Mattermost; @@ -32,6 +33,7 @@ export default function EditInstallationModal({ installation, onSubmit, show, on fileStore: installation?.spec.fileStore, fileStorePatch: {}, databasePatch: {}, + mattermostEnv: installation?.spec.mattermostEnv || [], } as PatchMattermostWorkspaceRequest); const handleChange = (e: React.ChangeEvent, field: string) => { @@ -39,7 +41,11 @@ export default function EditInstallationModal({ installation, onSubmit, show, on setInstallationPatch({ ...installationPatch, [field]: value}); } - const buildExistingFilestoreObject = (filestore: FileStore) => { + const buildExistingFilestoreObject = (filestore: FileStore | undefined) => { + if (!filestore) { + return undefined; + } + if (filestore.local) { return { local: filestore.local as LocalFileStore @@ -47,10 +53,10 @@ export default function EditInstallationModal({ installation, onSubmit, show, on } else if (filestore.external) { return { external: { - ...filestore.external, - accessKeySecret: installationSecrets?.filestoreSecret.data.secretkey, - accessKeyId: installationSecrets?.filestoreSecret.data.accesskey, - + url: filestore.external.url || '', + bucket: filestore.external.bucket || '', + accessKeySecret: installationSecrets?.filestoreSecret?.data?.secretkey || '', + accessKeyId: installationSecrets?.filestoreSecret?.data?.accesskey || '', } as S3FileStore } as FileStore; } else if (filestore.externalVolume) { @@ -58,6 +64,8 @@ export default function EditInstallationModal({ installation, onSubmit, show, on externalVolume: filestore.externalVolume } as FileStore; } + + return undefined; } const buildExistingDatabaseObject = (database: ExistingDBConnection) => { @@ -87,9 +95,13 @@ export default function EditInstallationModal({ installation, onSubmit, show, on setInstallationPatch({...installationPatch, databasePatch: change}); } + const handleEnvVariablesChange = (envVars: MattermostEnvItem[]) => { + setInstallationPatch({...installationPatch, mattermostEnv: envVars}); + } + return ( - + Edit installation Update installation {installationPatch?.name} @@ -105,8 +117,12 @@ export default function EditInstallationModal({ installation, onSubmit, show, on handleChange(e, 'endpoint')}/> {isSuccess &&