From 53cac3bc06b02df8ff78218f9e7cc8dbd0a1a0b1 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 13:59:16 -0400 Subject: [PATCH 01/13] Add support for AWS session token --- api/bootstrapper.go | 59 ++++++++++++------- model/cluster.go | 1 + model/mattermost.go | 22 +++++-- providers/aws.go | 8 ++- webapp/src/pages/aws/choose_existing.tsx | 18 ++++-- .../src/pages/mattermost/create_workspace.tsx | 15 ++++- webapp/src/pages/mattermost/db_connection.tsx | 45 ++++++++++---- .../pages/mattermost/filestore_connection.tsx | 41 ++++++++++--- webapp/src/pages/setup/get_credentials.tsx | 18 +++++- .../store/installation/bootstrapperSlice.ts | 2 +- webapp/src/types/Installation.ts | 3 +- webapp/src/types/bootstrapper.ts | 1 + 12 files changed, 172 insertions(+), 61 deletions(-) diff --git a/api/bootstrapper.go b/api/bootstrapper.go index 5c5a19d..8add6e3 100644 --- a/api/bootstrapper.go +++ b/api/bootstrapper.go @@ -1003,6 +1003,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{ @@ -1060,29 +1061,37 @@ 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 + }, + } - // 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 @@ -1108,6 +1117,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{ @@ -1127,7 +1139,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, 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 c217f30..1fc4c38 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"` } @@ -170,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{ @@ -191,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{ @@ -290,7 +300,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 } @@ -306,7 +316,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 eb33bea..65e892d 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 { diff --git a/webapp/src/pages/aws/choose_existing.tsx b/webapp/src/pages/aws/choose_existing.tsx index c8004c0..172bb68 100644 --- a/webapp/src/pages/aws/choose_existing.tsx +++ b/webapp/src/pages/aws/choose_existing.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Button, Option, Select } from '@mui/joy'; import { useDispatch } from 'react-redux'; @@ -22,8 +22,9 @@ export default function ExistingAWSPage() { const clusterName = useSelector((state: RootState) => state.aws.clusterName); const region = useSelector((state: RootState) => state.aws.region); - const {data: possibleClusters, isLoading, isError, isSuccess} = useGetPossibleClustersQuery({cloudProvider, region}, { - skip: cloudProvider === '' || (cloudProvider !== 'custom' && !region), + const skipQuery = cloudProvider === '' || (cloudProvider !== 'custom' && !region); + const {data: possibleClusters, isLoading, isError, isSuccess, refetch} = useGetPossibleClustersQuery({cloudProvider, region}, { + skip: skipQuery, }); const {isLoading: clusterLoading, isError: clusterError, isSuccess: clusterSuccess} = useGetClusterQuery({cloudProvider, clusterName}, { @@ -31,6 +32,12 @@ export default function ExistingAWSPage() { }); + useEffect(() => { + if (!skipQuery) { + refetch(); + } + }, [skipQuery, refetch]); + const onClickContinue = () => { if (cloudProvider !== 'custom' && !region) { updateRegion({region, cloudProvider}); @@ -67,7 +74,7 @@ export default function ExistingAWSPage() { {cloudProvider !== 'custom' && <> - { dispatch(setRegion(newValue)) }} size="sm" placeholder="AWS Region"> {Object.values(AWSRegions).map(region => ( ))} @@ -77,6 +84,9 @@ export default function ExistingAWSPage() { { dispatch(setEksClusterName(newValue) as any) }} clusters={possibleClusters || []} />
+ {(isSuccess && !possibleClusters?.length) && ( + + )} {/* TODO: Uncomment when we fully support creating new */} {clusterName && }
diff --git a/webapp/src/pages/mattermost/create_workspace.tsx b/webapp/src/pages/mattermost/create_workspace.tsx index b0a2b63..84b5b5a 100644 --- a/webapp/src/pages/mattermost/create_workspace.tsx +++ b/webapp/src/pages/mattermost/create_workspace.tsx @@ -33,11 +33,22 @@ export default function CreateWorkspacePage() { const informationFetched = isGetClusterSuccess && isGetReleasesSuccess; const handleCreateWorkspace = () => { - createWorkspace({ clusterName, cloudProvider, workspaceInfo: { ...filestoreConnection, ...dbConnection, ...workspaceInfo } }) + createWorkspace({ + clusterName, + cloudProvider, + workspaceInfo: { + ...workspaceInfo, + ...filestoreConnection, + ...dbConnection, + } + }) }; const filestoreComplete = () => { if (filestoreConnection.filestoreOption === 'ExistingS3') { + if (filestoreConnection.filestoreSecretName) { + return true; + } return !!filestoreConnection.s3FilestoreConfig?.url && !!filestoreConnection.s3FilestoreConfig?.bucket && !!filestoreConnection.s3FilestoreConfig?.accessKeyId && !!filestoreConnection.s3FilestoreConfig?.accessKeySecret; } else if (filestoreConnection.filestoreOption === 'InClusterLocal') { return filestoreConnection.localFilestoreConfig?.storageSize ? true : false; @@ -52,7 +63,7 @@ export default function CreateWorkspacePage() { let workspaceInfoComplete = false; const filestoreConnectionComplete = filestoreComplete(); - if (dbConnection.dbConnectionOption === 'Existing' && !!dbConnection.existingDatabaseConfig?.dbConnectionString && !!dbConnection.existingDatabaseConfig?.dbReplicasConnectionString) { + if (dbConnection.dbConnectionOption === 'Existing' && (dbConnection.existingDatabaseSecretName || (!!dbConnection.existingDatabaseConfig?.dbConnectionString && !!dbConnection.existingDatabaseConfig?.dbReplicasConnectionString))) { dbConnectionComplete = true; } diff --git a/webapp/src/pages/mattermost/db_connection.tsx b/webapp/src/pages/mattermost/db_connection.tsx index d6f763b..2831385 100644 --- a/webapp/src/pages/mattermost/db_connection.tsx +++ b/webapp/src/pages/mattermost/db_connection.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { Release } from '../../types/bootstrapper'; -import { Input, Option, Select } from '@mui/joy'; +import { Input, Option, Select, Switch } from '@mui/joy'; import { DatabaseType, ExistingDBConnection } from '../../types/Installation'; import './db_connection.scss'; import SensitiveInput from '../../components/common/text_inputs/sensitive_input'; @@ -8,36 +8,39 @@ import SensitiveInput from '../../components/common/text_inputs/sensitive_input' type DBConnectionInputProps = { releases: Release[]; cloudProvider: string; - onChange: ({ existingDatabaseConfig, dbConnectionOption }: DBConnectionDetails) => void; + onChange: ({ existingDatabaseConfig, dbConnectionOption, existingDatabaseSecretName }: DBConnectionDetails) => void; existingDatabase?: ExistingDBConnection; + existingDatabaseSecretName?: string; isEdit?: boolean; } -export type DBConnectionDetails = { existingDatabaseConfig?: ExistingDBConnection, dbConnectionOption: string } +export type DBConnectionDetails = { existingDatabaseConfig?: ExistingDBConnection, dbConnectionOption: string, existingDatabaseSecretName?: string } -export default function DBConnection({ releases, onChange, cloudProvider, isEdit, existingDatabase }: DBConnectionInputProps) { +export default function DBConnection({ releases, onChange, cloudProvider, isEdit, existingDatabase, existingDatabaseSecretName }: DBConnectionInputProps) { const hasDeployedPGOperator = releases?.some((release) => release.Name === 'cnpg-system'); const [existingDatabaseConfig, setExistingDatabaseConfig] = React.useState(() => { if (isEdit && existingDatabase) return existingDatabase; return undefined; }); + const [useExistingSecret, setUseExistingSecret] = React.useState(() => !!existingDatabaseSecretName); + const [existingSecretName, setExistingSecretName] = React.useState(existingDatabaseSecretName || ''); const [databaseOption, setDatabaseOption] = React.useState(() => { if (existingDatabase && isEdit) return DatabaseType.Existing; return ''; } ); - const resetForm = () => { - setExistingDatabaseConfig(undefined); - } - useEffect(() => { - resetForm(); + if (databaseOption !== DatabaseType.Existing) { + setExistingDatabaseConfig(undefined); + setUseExistingSecret(false); + setExistingSecretName(''); + } }, [databaseOption]) useEffect(() => { - onChange({ existingDatabaseConfig, dbConnectionOption: databaseOption }); - }, [existingDatabaseConfig, databaseOption, onChange]); + onChange({ existingDatabaseConfig, dbConnectionOption: databaseOption, existingDatabaseSecretName: useExistingSecret ? existingSecretName : undefined }); + }, [existingDatabaseConfig, databaseOption, existingSecretName, useExistingSecret, onChange]); const handleExistingDBChange = (field: string, value: string) => { setExistingDatabaseConfig({ ...existingDatabaseConfig, [field]: value } as ExistingDBConnection); @@ -49,8 +52,24 @@ export default function DBConnection({ releases, onChange, cloudProvider, isEdit return ( <> {!isEdit &&
Connect to an externally managed database through a connection string
} - handleExistingDBChange('dbConnectionString', value)} /> - handleExistingDBChange('dbReplicasConnectionString', value)} /> +
+ { + const checked = event.target.checked; + setUseExistingSecret(checked); + if (!checked) { + setExistingSecretName(''); + } + }} /> + Use existing Kubernetes secret +
+ {useExistingSecret ? ( + setExistingSecretName(value)} /> + ) : ( + <> + handleExistingDBChange('dbConnectionString', value)} /> + handleExistingDBChange('dbReplicasConnectionString', value)} /> + + )} ) case DatabaseType.CreateCNPG: diff --git a/webapp/src/pages/mattermost/filestore_connection.tsx b/webapp/src/pages/mattermost/filestore_connection.tsx index 1fbc7bb..3829536 100644 --- a/webapp/src/pages/mattermost/filestore_connection.tsx +++ b/webapp/src/pages/mattermost/filestore_connection.tsx @@ -1,18 +1,18 @@ import React, { useEffect } from 'react'; -import { Input, Option, Select } from '@mui/joy'; +import { Input, Option, Select, Switch } from '@mui/joy'; import { FileStore, FilestoreType, LocalExternalFileStore, LocalFileStore, S3FileStore } from '../../types/Installation'; import './filestore_connection.scss'; type FilestoreConnectionProps = { - onChange: ({filestoreOption, s3FilestoreConfig, localFilestoreConfig, localExternalFilestoreConfig}: FilestoreConnectionDetails) => void; + onChange: ({filestoreOption, s3FilestoreConfig, localFilestoreConfig, localExternalFilestoreConfig, filestoreSecretName}: FilestoreConnectionDetails) => void; cloudProvider: string; existingFilestore?: FileStore; isEdit?: boolean; } -export type FilestoreConnectionDetails = {filestoreOption: string, localFilestoreConfig?: LocalFileStore, s3FilestoreConfig?: S3FileStore, localExternalFilestoreConfig?: LocalExternalFileStore}; +export type FilestoreConnectionDetails = {filestoreOption: string, localFilestoreConfig?: LocalFileStore, s3FilestoreConfig?: S3FileStore, localExternalFilestoreConfig?: LocalExternalFileStore, filestoreSecretName?: string}; export default function FilestoreConnection({onChange, cloudProvider, existingFilestore, isEdit}: FilestoreConnectionProps) { const [filestoreOption, setFilestoreOption] = React.useState(() => { @@ -25,10 +25,12 @@ export default function FilestoreConnection({onChange, cloudProvider, existingFi const [s3FilestoreConfig, setS3FilestoreConfig] = React.useState(existingFilestore?.external); const [localFilestoreConfig, setLocalFilestoreConfig] = React.useState(existingFilestore?.local); const [localExternalFilestoreConfig, setLocalExternalFilestoreConfig] = React.useState(existingFilestore?.externalVolume); + const [useExistingSecret, setUseExistingSecret] = React.useState(() => !!existingFilestore?.external?.secret); + const [existingSecretName, setExistingSecretName] = React.useState(existingFilestore?.external?.secret || ''); useEffect(() => { - onChange({filestoreOption, localFilestoreConfig, s3FilestoreConfig, localExternalFilestoreConfig}); - }, [filestoreOption, localFilestoreConfig, s3FilestoreConfig, localExternalFilestoreConfig, onChange]) + onChange({filestoreOption, localFilestoreConfig, s3FilestoreConfig, localExternalFilestoreConfig, filestoreSecretName: existingSecretName}); + }, [filestoreOption, localFilestoreConfig, s3FilestoreConfig, localExternalFilestoreConfig, existingSecretName, onChange]) useEffect(() => { resetForm(); @@ -37,6 +39,8 @@ export default function FilestoreConnection({onChange, cloudProvider, existingFi const resetForm = () => { setLocalFilestoreConfig(undefined); setS3FilestoreConfig(undefined); + setUseExistingSecret(false); + setExistingSecretName(''); } const handleExistingS3Change = (field:string, value: string) => { @@ -53,10 +57,29 @@ export default function FilestoreConnection({onChange, cloudProvider, existingFi return ( <> {!existingFilestore &&
Provide connection details for your existing S3 bucket.
} - handleExistingS3Change('url', e.target.value)} placeholder="Filestore URL" /> - handleExistingS3Change('bucket', e.target.value)} placeholder="Bucket Name" /> - handleExistingS3Change('accessKeyId', e.target.value)} placeholder="Access Key ID" /> - handleExistingS3Change('accessKeySecret', e.target.value)} placeholder="Access Key Secret" /> +
+ { + const checked = event.target.checked; + setUseExistingSecret(checked); + if (checked) { + setS3FilestoreConfig(undefined); + } + if (!checked) { + setExistingSecretName(''); + } + }} /> + Use existing Kubernetes secret +
+ {useExistingSecret ? ( + setExistingSecretName(e.target.value)} placeholder="Existing Secret Name" /> + ) : ( + <> + handleExistingS3Change('url', e.target.value)} placeholder="Filestore URL" /> + handleExistingS3Change('bucket', e.target.value)} placeholder="Bucket Name" /> + handleExistingS3Change('accessKeyId', e.target.value)} placeholder="Access Key ID" /> + handleExistingS3Change('accessKeySecret', e.target.value)} placeholder="Access Key Secret" /> + + )} ) case FilestoreType.InClusterExternal: diff --git a/webapp/src/pages/setup/get_credentials.tsx b/webapp/src/pages/setup/get_credentials.tsx index b62af1c..03d94e6 100644 --- a/webapp/src/pages/setup/get_credentials.tsx +++ b/webapp/src/pages/setup/get_credentials.tsx @@ -5,13 +5,14 @@ import { CSSTransition } from 'react-transition-group'; type Props = { cloudProvider: string; kubernetesOption: string; - onCredentialsChange: (credentials: { accessKeyId: string, accessKeySecret: string, kubeconfig: string }) => void; + onCredentialsChange: (credentials: { accessKeyId: string, accessKeySecret: string, sessionToken: string, kubeconfig: string }) => void; }; function GetCredentials({ cloudProvider, kubernetesOption, onCredentialsChange }: Props) { const [credentials, setCredentials] = useState({ accessKeyId: '', accessKeySecret: '', + sessionToken: '', kubeconfig: '', kubeconfigType: '', }); @@ -77,6 +78,15 @@ function GetCredentials({ cloudProvider, kubernetesOption, onCredentialsChange } )} + {(cloudProvider !== 'custom' && kubernetesOption === 'new') && ( +
+
+ + +
+
+ )} + {cloudProvider === 'custom' && ( <> @@ -104,6 +114,12 @@ function GetCredentials({ cloudProvider, kubernetesOption, onCredentialsChange } +
+
+ + +
+
)} diff --git a/webapp/src/store/installation/bootstrapperSlice.ts b/webapp/src/store/installation/bootstrapperSlice.ts index b9e70f6..596f850 100644 --- a/webapp/src/store/installation/bootstrapperSlice.ts +++ b/webapp/src/store/installation/bootstrapperSlice.ts @@ -14,7 +14,7 @@ export interface BootstrapperState { const initialState: BootstrapperState = { cloudProvider: '', kubernetesOption: '', - cloudCredentials: { accessKeyId: '', accessKeySecret: '', region: '', kubeconfig: '', kubeconfigType: '' }, + cloudCredentials: { accessKeyId: '', accessKeySecret: '', sessionToken: '', region: '', kubeconfig: '', kubeconfigType: '' }, utilities: allUtilities, } diff --git a/webapp/src/types/Installation.ts b/webapp/src/types/Installation.ts index d29a628..4599434 100644 --- a/webapp/src/types/Installation.ts +++ b/webapp/src/types/Installation.ts @@ -28,7 +28,8 @@ export interface S3FileStore { url: string; accessKeyId: string; accessKeySecret: string; - bucket: string + bucket: string; + secret?: string; } export interface PatchMattermostWorkspaceRequest { diff --git a/webapp/src/types/bootstrapper.ts b/webapp/src/types/bootstrapper.ts index cb3867c..b8c4a22 100644 --- a/webapp/src/types/bootstrapper.ts +++ b/webapp/src/types/bootstrapper.ts @@ -10,6 +10,7 @@ export type State = { export type CloudCredentials = { accessKeyId: string; accessKeySecret: string; + sessionToken: string; region: string; kubeconfig: string; kubeconfigType: string; From 0500b6ee049d4742ee5e561e2e81aefeca72270f Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 14:07:02 -0400 Subject: [PATCH 02/13] Change operator spec version --- api/bootstrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/bootstrapper.go b/api/bootstrapper.go index 8add6e3..57c42e3 100644 --- a/api/bootstrapper.go +++ b/api/bootstrapper.go @@ -1234,7 +1234,7 @@ func handleDeployMattermostOperator(c *Context, w http.ResponseWriter, r *http.R ChartName: "mattermost/mattermost-operator", Namespace: "mattermost-operator", UpgradeCRDs: true, - Version: "v1.22.0", + Version: "v1.25.2", Wait: true, Timeout: 300 * time.Second, CreateNamespace: true, From 0adaea3bd1c1445c8cc76a25ef71dc9b5d6f8625 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 14:08:36 -0400 Subject: [PATCH 03/13] use latest --- api/bootstrapper.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/bootstrapper.go b/api/bootstrapper.go index 57c42e3..6c69b1b 100644 --- a/api/bootstrapper.go +++ b/api/bootstrapper.go @@ -1230,11 +1230,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.25.2", + ReleaseName: "mattermost-operator", + ChartName: "mattermost/mattermost-operator", + Namespace: "mattermost-operator", + UpgradeCRDs: true, + // Version: "1.25.2", Wait: true, Timeout: 300 * time.Second, CreateNamespace: true, From 0b125e0fe61c573fad742ce8948720e1b62bb559 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 14:22:40 -0400 Subject: [PATCH 04/13] Disable telemetry --- api/bootstrapper.go | 2 +- api/context.go | 10 +++--- api/telemetry.go | 76 ++++++++++++++++++++++----------------------- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/api/bootstrapper.go b/api/bootstrapper.go index 6c69b1b..644bca3 100644 --- a/api/bootstrapper.go +++ b/api/bootstrapper.go @@ -425,7 +425,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", diff --git a/api/context.go b/api/context.go index d12e830..7cd0502 100644 --- a/api/context.go +++ b/api/context.go @@ -75,17 +75,17 @@ 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 - } + // 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, + TelemetryProvider: nil, }, nil } diff --git a/api/telemetry.go b/api/telemetry.go index 0f3e1cd..19e89b5 100644 --- a/api/telemetry.go +++ b/api/telemetry.go @@ -1,11 +1,9 @@ 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" ) @@ -23,19 +21,19 @@ func initTelemetry(apiRouter *mux.Router, context *Context) { 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 := 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 - } + // 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) } @@ -43,19 +41,19 @@ func handleTrack(c *Context, w http.ResponseWriter, r *http.Request) { 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 := 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 - } + // 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) } @@ -63,19 +61,19 @@ func handleIdentify(c *Context, w http.ResponseWriter, r *http.Request) { 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 - } + // 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) } From 2c5f514fc9776aced7c508948826303a693d5025 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 14:23:03 -0400 Subject: [PATCH 05/13] Really disable --- api/telemetry.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/telemetry.go b/api/telemetry.go index 19e89b5..b2731a7 100644 --- a/api/telemetry.go +++ b/api/telemetry.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/gorilla/mux" - "github.com/mattermost/mattermost-cloudnative-bootstrapper/telemetry" ) func initTelemetry(apiRouter *mux.Router, context *Context) { @@ -19,7 +18,7 @@ func initTelemetry(apiRouter *mux.Router, context *Context) { } func handleTrack(c *Context, w http.ResponseWriter, r *http.Request) { - var track telemetry.Track + // var track telemetry.Track // err := json.NewDecoder(r.Body).Decode(&track) // if err != nil { @@ -39,7 +38,7 @@ func handleTrack(c *Context, w http.ResponseWriter, r *http.Request) { } func handleIdentify(c *Context, w http.ResponseWriter, r *http.Request) { - var identify telemetry.Identify + // var identify telemetry.Identify // err := json.NewDecoder(r.Body).Decode(&identify) // if err != nil { @@ -59,7 +58,7 @@ func handleIdentify(c *Context, w http.ResponseWriter, r *http.Request) { } func handlePage(c *Context, w http.ResponseWriter, r *http.Request) { - var page telemetry.Page + // var page telemetry.Page // err := json.NewDecoder(r.Body).Decode(&page) // if err != nil { From 2bfae1272e57a93a8fd8aa538bebb335e0d2f3b0 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 16:27:31 -0400 Subject: [PATCH 06/13] Strip out telemetry --- api/api.go | 1 - api/context.go | 10 --- api/telemetry.go | 78 ----------------- telemetry/telemetry.go | 111 ------------------------- webapp/src/App.tsx | 3 - webapp/src/client/telemetryApi.ts | 36 -------- webapp/src/hooks/useRudderIdentify.tsx | 10 --- webapp/src/hooks/useRudderPage.tsx | 10 --- webapp/src/hooks/useRudderTrack.tsx | 11 --- webapp/src/store/index.ts | 4 +- 10 files changed, 1 insertion(+), 273 deletions(-) delete mode 100644 api/telemetry.go delete mode 100644 telemetry/telemetry.go delete mode 100644 webapp/src/client/telemetryApi.ts delete mode 100644 webapp/src/hooks/useRudderIdentify.tsx delete mode 100644 webapp/src/hooks/useRudderPage.tsx delete mode 100644 webapp/src/hooks/useRudderTrack.tsx 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/context.go b/api/context.go index 7cd0502..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: nil, }, 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/telemetry.go b/api/telemetry.go deleted file mode 100644 index b2731a7..0000000 --- a/api/telemetry.go +++ /dev/null @@ -1,78 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/gorilla/mux" -) - -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/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/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/hooks/useRudderIdentify.tsx b/webapp/src/hooks/useRudderIdentify.tsx deleted file mode 100644 index 346312f..0000000 --- a/webapp/src/hooks/useRudderIdentify.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { useRudderIdentifyMutation } from '../client/telemetryApi'; - -export default function useRudderIdentify() { - const [rudderIdentify,] = useRudderIdentifyMutation(); - - return (userId: string, traits: Record) => { - rudderIdentify({ userId, traits }); - } -} \ No newline at end of file diff --git a/webapp/src/hooks/useRudderPage.tsx b/webapp/src/hooks/useRudderPage.tsx deleted file mode 100644 index 3a5107d..0000000 --- a/webapp/src/hooks/useRudderPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { useRudderPageMutation } from '../client/telemetryApi'; - -export default function useRudderPage() { - const [rudderPage,] = useRudderPageMutation(); - - return (category: string, name: string, properties: Record) => { - rudderPage({ category, name, properties }); - } -} \ No newline at end of file diff --git a/webapp/src/hooks/useRudderTrack.tsx b/webapp/src/hooks/useRudderTrack.tsx deleted file mode 100644 index b32cdb1..0000000 --- a/webapp/src/hooks/useRudderTrack.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { useRudderTrackMutation } from '../client/telemetryApi'; - - -export default function useRudderTrack() { - const [rudderTrack,] = useRudderTrackMutation(); - - return (event: string, properties: Record) => { - rudderTrack({ event, properties }); - } -} \ No newline at end of file diff --git a/webapp/src/store/index.ts b/webapp/src/store/index.ts index 51cfa01..6d4f686 100644 --- a/webapp/src/store/index.ts +++ b/webapp/src/store/index.ts @@ -4,7 +4,6 @@ import awsSlice from './installation/awsSlice' import dashboardSlice from './installation/dashboardSlice' import { dashboardApi } from '../client/dashboardApi' import {bootstrapperApi} from '../client/bootstrapperApi' -import { telemetryApi } from '../client/telemetryApi' export const store = configureStore({ reducer: { @@ -13,10 +12,9 @@ export const store = configureStore({ aws: awsSlice, [dashboardApi.reducerPath]: dashboardApi.reducer, [bootstrapperApi.reducerPath]: bootstrapperApi.reducer, - [telemetryApi.reducerPath]: telemetryApi.reducer, }, middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(dashboardApi.middleware).concat(bootstrapperApi.middleware).concat(telemetryApi.middleware), + getDefaultMiddleware().concat(dashboardApi.middleware).concat(bootstrapperApi.middleware), }) // Infer the `RootState` and `AppDispatch` types from the store itself From 456f6067a61deb5a04dbe5ac2b8036d5cd747ab9 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 21:54:29 -0400 Subject: [PATCH 07/13] fix aws kube credential issues --- providers/aws.go | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/providers/aws.go b/providers/aws.go index 65e892d..96303d9 100644 --- a/providers/aws.go +++ b/providers/aws.go @@ -359,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 } @@ -398,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 } From 2e3b6a9cf1d721ceb5a017709b99aade25208f05 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Wed, 1 Oct 2025 22:25:12 -0400 Subject: [PATCH 08/13] Add better support for multiple users leveraging the bootstrapper at once --- api/state.go | 37 ++++++++++++++++++++++++ webapp/src/client/bootstrapperApi.ts | 4 +++ webapp/src/pages/setup/index.tsx | 43 ++++++++++++++++++++++++---- webapp/src/pages/setup/setup.scss | 19 ++++++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/api/state.go b/api/state.go index 002e10e..c19cbe6 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) 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/pages/setup/index.tsx b/webapp/src/pages/setup/index.tsx index 53c1652..c1fc516 100644 --- a/webapp/src/pages/setup/index.tsx +++ b/webapp/src/pages/setup/index.tsx @@ -9,7 +9,7 @@ import { useSelector } from 'react-redux'; import { RootState } from '../../store'; import { setCloudCredentials, setCloudProvider, setKubernetesOption } from '../../store/installation/bootstrapperSlice'; import { useNavigate } from 'react-router-dom'; -import { useSetAndCheckCloudCredentialsMutation } from '../../client/bootstrapperApi'; +import { useSetAndCheckCloudCredentialsMutation, useCheckExistingSessionQuery } from '../../client/bootstrapperApi'; import RTKConnectedLoadingSpinner from '../../components/common/rtk_connected_loading_spinner'; export default function SetupPage() { @@ -23,6 +23,10 @@ export default function SetupPage() { const lastPageLocalStorage = localStorage.getItem('lastVisitedPage'); const [lastVisitedPage, setShowLastVisitedPage] = useState(lastPageLocalStorage); const showLastVisitedPage = lastVisitedPage && lastVisitedPage !== '/' ? true : false; + + // Check for existing server-side session + const { data: sessionData, isLoading: isSessionLoading } = useCheckExistingSessionQuery(); + const hasServerSession = sessionData?.hasState && sessionData.provider && sessionData.clusterName; useEffect(() => { if (cloudProvider === 'custom') { @@ -46,6 +50,15 @@ export default function SetupPage() { } } + const handleContinueServerSession = () => { + if (sessionData?.provider && sessionData?.clusterName) { + // Navigate to the dashboard for the existing session + navigate(`/${sessionData.provider}/dashboard?clusterName=${sessionData.clusterName}`); + } + } + + const showResumeOptions = showLastVisitedPage || hasServerSession; + return (
@@ -61,14 +74,32 @@ export default function SetupPage() {

Setup

- {showLastVisitedPage && + {isSessionLoading && } + + {!isSessionLoading && showResumeOptions &&
- - + {hasServerSession && ( + <> +
+

Existing session found: {sessionData.provider} cluster {sessionData.clusterName}

+
+ + + )} + {showLastVisitedPage && ( + + )} +
} - { - !showLastVisitedPage && + + {!isSessionLoading && !showResumeOptions && <> handleChange(e, 'endpoint')}/> {isSuccess &&