diff --git a/api/datastore/migrator/migrate_2_43_0.go b/api/datastore/migrator/migrate_2_43_0.go index 6455ee61be..f45dc722d6 100644 --- a/api/datastore/migrator/migrate_2_43_0.go +++ b/api/datastore/migrator/migrate_2_43_0.go @@ -24,7 +24,6 @@ type legacyGitAuthentication struct { Password string Provider int `json:",omitempty"` AuthorizationType int `json:",omitempty"` - GitCredentialID int } func (lrc *legacyRepoConfig) toRepoConfig() *gittypes.RepoConfig { @@ -41,12 +40,6 @@ func (lrc *legacyRepoConfig) toRepoConfig() *gittypes.RepoConfig { } if lrc.Authentication != nil { - if lrc.Authentication.GitCredentialID != 0 { - log.Warn(). - Int("git_credential_id", lrc.Authentication.GitCredentialID). - Msg("stack has a GitCredentialID reference which is not supported in CE; credential reference will be dropped during migration") - } - cfg.Authentication = &gittypes.GitAuthentication{ Username: lrc.Authentication.Username, Password: lrc.Authentication.Password, @@ -213,14 +206,6 @@ func (m *Migrator) migrateCustomTemplateGitConfigToSources_2_43_0() error { TLSSkipVerify: t.GitConfig.TLSSkipVerify, } - if cfg.Authentication != nil && cfg.Authentication.GitCredentialID != 0 { - log.Warn(). - Int("git_credential_id", cfg.Authentication.GitCredentialID). - Msg("custom template has a GitCredentialID reference which is not supported in CE; credential reference will be dropped during migration") - - cfg.Authentication.GitCredentialID = 0 - } - key := gitSourceKey(cfg) var newSrcID portainer.SourceID diff --git a/api/git/types/types.go b/api/git/types/types.go index 521c3bb32d..f3f1294e75 100644 --- a/api/git/types/types.go +++ b/api/git/types/types.go @@ -96,8 +96,4 @@ type GitAuthentication struct { Password string Provider GitProvider `json:",omitempty"` AuthorizationType GitCredentialAuthType `json:",omitempty"` - // Git credentials identifier when the value is not 0 - // When the value is 0, Username and Password are set without using saved credential - // This is introduced since 2.15.0 - GitCredentialID int `example:"0"` } diff --git a/api/gitops/workflows/source_artifact.go b/api/gitops/workflows/source_artifact.go index c592a20ecc..f3a5b2ded2 100644 --- a/api/gitops/workflows/source_artifact.go +++ b/api/gitops/workflows/source_artifact.go @@ -10,6 +10,7 @@ import ( ) // gitSourceStore is the minimal intersection of CE and EE DataStoreTx that these functions need. +// Both EE and CE DataStoreTx satisfy it, even though they are incompatible as full interface types. type gitSourceStore interface { Workflow() dataservices.WorkflowService Source() dataservices.SourceService @@ -345,7 +346,7 @@ func gitAuthMatches(a, b *gittypes.GitAuthentication) bool { return false } - return a.Username == b.Username && a.Password == b.Password && a.GitCredentialID == b.GitCredentialID + return a.Username == b.Username && a.Password == b.Password } // ValidateUniqueSource validates there are no other sources with the same URL and credentials. diff --git a/api/gitops/workflows/source_artifact_test.go b/api/gitops/workflows/source_artifact_test.go index 5437e24e48..44406a4ccf 100644 --- a/api/gitops/workflows/source_artifact_test.go +++ b/api/gitops/workflows/source_artifact_test.go @@ -751,6 +751,186 @@ func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) { require.Equal(t, "hash-2", wf.Artifacts[1].Files[0].Hash) } +func TestUpdateArtifactFileForStack_MultipleArtifactsOnlyMatchingUpdated(t *testing.T) { + t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) + + var workflowID portainer.WorkflowID + var srcID portainer.SourceID + err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}} + err := tx.Source().Create(src) + require.NoError(t, err) + srcID = src.ID + + wf := &portainer.Workflow{ + Artifacts: []portainer.Artifact{ + {StackID: 10, Files: []portainer.ArtifactFile{{SourceID: srcID, Hash: "hash-10"}}}, + {StackID: 20, Files: []portainer.ArtifactFile{{SourceID: srcID, Hash: "hash-20"}}}, + }, + } + err = tx.Workflow().Create(wf) + require.NoError(t, err) + workflowID = wf.ID + + return nil + }) + require.NoError(t, err) + + err = store.UpdateTx(func(tx dataservices.DataStoreTx) error { + return UpdateArtifactFileForStack(tx, workflowID, 10, srcID, func(a *portainer.ArtifactFile) { + a.Hash = "updated-hash-10" + }) + }) + require.NoError(t, err) + + wf, err := store.Workflow().Read(workflowID) + require.NoError(t, err) + require.Equal(t, "updated-hash-10", wf.Artifacts[0].Files[0].Hash) + require.Equal(t, "hash-20", wf.Artifacts[1].Files[0].Hash) +} + +func TestUpdateArtifactFileForEdgeStack_MultipleArtifactsOnlyMatchingUpdated(t *testing.T) { + t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) + + var workflowID portainer.WorkflowID + var srcID portainer.SourceID + err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}} + err := tx.Source().Create(src) + require.NoError(t, err) + srcID = src.ID + + wf := &portainer.Workflow{ + Artifacts: []portainer.Artifact{ + {EdgeStackID: 10, Files: []portainer.ArtifactFile{{SourceID: srcID, Hash: "hash-10"}}}, + {EdgeStackID: 20, Files: []portainer.ArtifactFile{{SourceID: srcID, Hash: "hash-20"}}}, + }, + } + err = tx.Workflow().Create(wf) + require.NoError(t, err) + workflowID = wf.ID + + return nil + }) + require.NoError(t, err) + + err = store.UpdateTx(func(tx dataservices.DataStoreTx) error { + return UpdateArtifactFileForEdgeStack(tx, workflowID, 10, srcID, func(a *portainer.ArtifactFile) { + a.Hash = "updated-hash-10" + }) + }) + require.NoError(t, err) + + wf, err := store.Workflow().Read(workflowID) + require.NoError(t, err) + require.Equal(t, "updated-hash-10", wf.Artifacts[0].Files[0].Hash) + require.Equal(t, "hash-20", wf.Artifacts[1].Files[0].Hash) +} + +func TestGitSourceAndArtifactForStack_MultipleArtifactsReturnsCorrectOne(t *testing.T) { + t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) + + var workflowID portainer.WorkflowID + err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { + gitSrc := &portainer.Source{ + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"}, + } + err := tx.Source().Create(gitSrc) + require.NoError(t, err) + + wf := &portainer.Workflow{ + Artifacts: []portainer.Artifact{ + {StackID: 10, Files: []portainer.ArtifactFile{{SourceID: gitSrc.ID, Ref: "refs/heads/main", Hash: "hash-10"}}}, + {StackID: 20, Files: []portainer.ArtifactFile{{SourceID: gitSrc.ID, Ref: "refs/heads/dev", Hash: "hash-20"}}}, + }, + } + err = tx.Workflow().Create(wf) + require.NoError(t, err) + workflowID = wf.ID + + return nil + }) + require.NoError(t, err) + + var src *portainer.Source + var file *portainer.ArtifactFile + err = store.ViewTx(func(tx dataservices.DataStoreTx) error { + var txErr error + src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 20) + return txErr + }) + require.NoError(t, err) + require.NotNil(t, src) + require.NotNil(t, file) + require.Equal(t, "refs/heads/dev", file.Ref) + require.Equal(t, "hash-20", file.Hash) +} + +func TestGitSourceAndArtifactForEdgeStack_MultipleArtifactsReturnsCorrectOne(t *testing.T) { + t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) + + var workflowID portainer.WorkflowID + err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { + gitSrc := &portainer.Source{ + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-edge-repo"}, + } + err := tx.Source().Create(gitSrc) + require.NoError(t, err) + + wf := &portainer.Workflow{ + Artifacts: []portainer.Artifact{ + {EdgeStackID: 10, Files: []portainer.ArtifactFile{{SourceID: gitSrc.ID, Ref: "refs/heads/main", Hash: "hash-10"}}}, + {EdgeStackID: 20, Files: []portainer.ArtifactFile{{SourceID: gitSrc.ID, Ref: "refs/heads/dev", Hash: "hash-20"}}}, + }, + } + err = tx.Workflow().Create(wf) + require.NoError(t, err) + workflowID = wf.ID + + return nil + }) + require.NoError(t, err) + + var src *portainer.Source + var file *portainer.ArtifactFile + err = store.ViewTx(func(tx dataservices.DataStoreTx) error { + var txErr error + src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, workflowID, 20) + return txErr + }) + require.NoError(t, err) + require.NotNil(t, src) + require.NotNil(t, file) + require.Equal(t, "refs/heads/dev", file.Ref) + require.Equal(t, "hash-20", file.Hash) +} + +func TestMergeSourceAndFile_ConfigHashComesFromFileNotSource(t *testing.T) { + t.Parallel() + + // ConfigHash must come from ArtifactFile.Hash, not src.Git. + // A Source shared by two stacks has one Git.ConfigHash field; + // if reads used it instead of ArtifactFile.Hash they would clobber each other. + src := &portainer.Source{ + Git: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + }, + } + file := &portainer.ArtifactFile{ + Hash: "artifact-hash", + } + + cfg := MergeSourceAndFile(src, file) + require.NotNil(t, cfg) + require.Equal(t, "artifact-hash", cfg.ConfigHash) +} + func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) { t.Parallel() _, store := datastore.MustNewTestStore(t, false, true) diff --git a/api/http/handler/customtemplates/customtemplate_update.go b/api/http/handler/customtemplates/customtemplate_update.go index 7aeea8e259..ca78a780aa 100644 --- a/api/http/handler/customtemplates/customtemplate_update.go +++ b/api/http/handler/customtemplates/customtemplate_update.go @@ -43,14 +43,10 @@ type customTemplateUpdatePayload struct { // Use authentication to clone the Git repository RepositoryAuthentication bool `example:"true"` // Username used in basic authentication. Required when RepositoryAuthentication is true - // and RepositoryGitCredentialID is 0. Ignored if RepositoryAuthType is token RepositoryUsername string `example:"myGitUsername"` // Password used in basic authentication or token used in token authentication. - // Required when RepositoryAuthentication is true and RepositoryGitCredentialID is 0 + // Required when RepositoryAuthentication is true RepositoryPassword string `example:"myGitPassword"` - // GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication - // is true and RepositoryUsername/RepositoryPassword are not provided - RepositoryGitCredentialID int `example:"0"` // Path to the Stack file inside the Git repository ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"` // Content of stack file diff --git a/api/stacks/stackbuilders/stack_payload.go b/api/stacks/stackbuilders/stack_payload.go index bb5d65e3bd..1a9a9bacdd 100644 --- a/api/stacks/stackbuilders/stack_payload.go +++ b/api/stacks/stackbuilders/stack_payload.go @@ -43,10 +43,8 @@ type RepositoryConfigPayload struct { // Use basic authentication to clone the Git repository Authentication bool `example:"true"` // Username used in basic authentication. Required when RepositoryAuthentication is true - // and RepositoryGitCredentialID is 0 Username string `example:"myGitUsername"` // Password used in basic authentication. Required when RepositoryAuthentication is true - // and RepositoryGitCredentialID is 0 Password string `example:"myGitPassword"` // TLSSkipVerify skips SSL verification when cloning the Git repository TLSSkipVerify bool `example:"false"` diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset.controller.ts b/app/portainer/components/forms/git-form/git-form-auth-fieldset.controller.ts index c60d7a2c03..7ff59703a8 100644 --- a/app/portainer/components/forms/git-form/git-form-auth-fieldset.controller.ts +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset.controller.ts @@ -1,13 +1,8 @@ import { IFormController } from 'angular'; import { FormikErrors } from 'formik'; -import { notifyError } from '@/portainer/services/notifications'; -import { IAuthenticationService } from '@/portainer/services/types'; import { GitAuthModel } from '@/react/portainer/gitops/types'; import { gitAuthValidation } from '@/react/portainer/gitops/AuthFieldset'; -import { GitCredential } from '@/react/portainer/account/git-credentials/types'; -import { getGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { validateForm } from '@@/form-components/validate-form'; @@ -18,10 +13,6 @@ export default class GitFormAuthFieldsetController { gitFormAuthFieldset?: IFormController; - gitCredentials: Array = []; - - Authentication: IAuthenticationService; - value?: GitAuthModel; isAuthEdit: boolean; @@ -29,12 +20,8 @@ export default class GitFormAuthFieldsetController { onChange?: (value: GitAuthModel) => void; /* @ngInject */ - constructor( - $async: (fn: () => Promise) => Promise, - Authentication: IAuthenticationService - ) { + constructor($async: (fn: () => Promise) => Promise) { this.$async = $async; - this.Authentication = Authentication; this.isAuthEdit = false; this.handleChange = this.handleChange.bind(this); @@ -65,7 +52,7 @@ export default class GitFormAuthFieldsetController { ); this.errors = await validateForm( - () => gitAuthValidation(this.gitCredentials, isAuthEdit, false), + () => gitAuthValidation(isAuthEdit, false), value ); if (this.errors && Object.keys(this.errors).length > 0) { @@ -79,24 +66,11 @@ export default class GitFormAuthFieldsetController { } async $onInit() { - if (isBE) { - try { - // Only BE version support /gitcredentials - this.gitCredentials = await getGitCredentials( - this.Authentication.getUserDetails().ID - ); - } catch (err) { - notifyError( - 'Failure', - err as Error, - 'Unable to retrieve user saved git credentials' - ); - } - } // this should never happen, but just in case if (!this.value) { throw new Error('GitFormController: value is required'); } + await this.runGitValidation(this.value, this.isAuthEdit); } } diff --git a/app/portainer/components/forms/git-form/git-form.controller.ts b/app/portainer/components/forms/git-form/git-form.controller.ts index 7033296d99..195a6cbc0b 100644 --- a/app/portainer/components/forms/git-form/git-form.controller.ts +++ b/app/portainer/components/forms/git-form/git-form.controller.ts @@ -3,11 +3,6 @@ import { FormikErrors } from 'formik'; import { DeployMethod, GitFormModel } from '@/react/portainer/gitops/types'; import { validateGitForm } from '@/react/portainer/gitops/GitForm'; -import { notifyError } from '@/portainer/services/notifications'; -import { IAuthenticationService } from '@/portainer/services/types'; -import { getGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { GitCredential } from '@/react/portainer/account/git-credentials/types'; -import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; export default class GitFormController { errors?: FormikErrors; @@ -16,10 +11,6 @@ export default class GitFormController { gitForm?: IFormController; - gitCredentials: Array = []; - - Authentication: IAuthenticationService; - value?: GitFormModel; onChange?: (value: GitFormModel) => void; @@ -29,12 +20,8 @@ export default class GitFormController { deployMethod?: DeployMethod; /* @ngInject */ - constructor( - $async: (fn: () => Promise) => Promise, - Authentication: IAuthenticationService - ) { + constructor($async: (fn: () => Promise) => Promise) { this.$async = $async; - this.Authentication = Authentication; this.handleChange = this.handleChange.bind(this); this.runGitFormValidation = this.runGitFormValidation.bind(this); @@ -67,7 +54,6 @@ export default class GitFormController { this.gitForm?.$setValidity('gitForm', true, this.gitForm); this.errors = await validateGitForm( - this.gitCredentials, value, isCreatedFromCustomTemplate, this.deployMethod @@ -79,20 +65,6 @@ export default class GitFormController { } async $onInit() { - if (isBE) { - try { - this.gitCredentials = await getGitCredentials( - this.Authentication.getUserDetails().ID - ); - } catch (err) { - notifyError( - 'Failure', - err as Error, - 'Unable to retrieve user saved git credentials' - ); - } - } - // this should never happen, but just in case if (!this.value) { throw new Error('GitFormController: value is required'); diff --git a/app/react/common/stacks/EditGitSettings/useUpdateGitStack.ts b/app/react/common/stacks/EditGitSettings/useUpdateGitStack.ts index febe2500dd..929a6b7541 100644 --- a/app/react/common/stacks/EditGitSettings/useUpdateGitStack.ts +++ b/app/react/common/stacks/EditGitSettings/useUpdateGitStack.ts @@ -5,8 +5,6 @@ import { updateGitStack } from '@/react/portainer/gitops/queries/useUpdateGitSta import { updateGitStackSettings } from '@/react/portainer/gitops/queries/useUpdateGitStackSettings'; import { queryKeys } from '@/react/common/stacks/queries/query-keys'; import { transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils'; -import { saveGitCredentialsIfNeeded } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation'; -import { useCurrentUser } from '@/react/hooks/useUser'; import { withError } from '@/react-tools/react-query'; import { FormValues } from './types'; @@ -19,18 +17,12 @@ interface MutationArgs { export function useUpdateGitStack(stack: Stack) { const queryClient = useQueryClient(); - const { user } = useCurrentUser(); return useMutation({ mutationFn: async ({ values, repullImageAndRedeploy, webhookId, }: MutationArgs) => { - const resolvedAuth = await saveGitCredentialsIfNeeded( - user.Id, - values.git - ); - const autoUpdate = transformAutoUpdateViewModel( values.git.AutoUpdate, webhookId @@ -40,11 +32,9 @@ export function useUpdateGitStack(stack: Stack) { RepositoryURL: values.git.RepositoryURL, ConfigFilePath: values.git.ComposeFilePathInRepository, RepositoryReferenceName: values.git.RepositoryReferenceName, - RepositoryAuthentication: resolvedAuth.RepositoryAuthentication, - RepositoryGitCredentialID: resolvedAuth.RepositoryGitCredentialID, - RepositoryUsername: resolvedAuth.RepositoryUsername, - RepositoryPassword: resolvedAuth.RepositoryPassword || undefined, - RepositoryAuthorizationType: resolvedAuth.RepositoryAuthorizationType, + RepositoryAuthentication: values.git.RepositoryAuthentication, + RepositoryUsername: values.git.RepositoryUsername, + RepositoryPassword: values.git.RepositoryPassword || undefined, TLSSkipVerify: values.git.TLSSkipVerify, AutoUpdate: autoUpdate, AdditionalFiles: values.git.AdditionalFiles, @@ -61,11 +51,9 @@ export function useUpdateGitStack(stack: Stack) { Env: values.env, Prune: values.prune, StackName: values.kube.name.trim() || undefined, - RepositoryAuthentication: resolvedAuth.RepositoryAuthentication, - RepositoryGitCredentialID: resolvedAuth.RepositoryGitCredentialID, - RepositoryUsername: resolvedAuth.RepositoryUsername, - RepositoryPassword: resolvedAuth.RepositoryPassword || undefined, - RepositoryAuthorizationType: resolvedAuth.RepositoryAuthorizationType, + RepositoryAuthentication: values.git.RepositoryAuthentication, + RepositoryUsername: values.git.RepositoryUsername, + RepositoryPassword: values.git.RepositoryPassword || undefined, RepullImageAndRedeploy: repullImageAndRedeploy, }); return { redeployAttempted: true, redeployFailed: false }; diff --git a/app/react/common/stacks/EditGitSettings/validation.ts b/app/react/common/stacks/EditGitSettings/validation.ts index f9bb4eb51e..949d353d94 100644 --- a/app/react/common/stacks/EditGitSettings/validation.ts +++ b/app/react/common/stacks/EditGitSettings/validation.ts @@ -3,8 +3,6 @@ import { boolean, object, SchemaOf, string } from 'yup'; import { StackType } from '@/react/common/stacks/types'; import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm'; -import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { useCurrentUser } from '@/react/hooks/useUser'; import { envVarValidation } from '@@/form-components/EnvironmentVariablesFieldset'; @@ -13,8 +11,6 @@ import { FormValues } from './types'; export function useValidationSchema( stackType: StackType ): SchemaOf { - const { user } = useCurrentUser(); - const gitCredentialsQuery = useGitCredentials(user.Id); const isKubernetes = stackType === StackType.Kubernetes; return useMemo( @@ -26,7 +22,6 @@ export function useValidationSchema( }).required() : object({ name: string().default('') }).optional(), git: buildGitValidationSchema( - gitCredentialsQuery.data || [], false, isKubernetes ? 'manifest' : 'compose', true @@ -36,6 +31,6 @@ export function useValidationSchema( prune: boolean().default(false), redeployNow: boolean().default(false), }), - [gitCredentialsQuery.data, isKubernetes] + [isKubernetes] ); } diff --git a/app/react/common/stacks/GitPullButton.tsx b/app/react/common/stacks/GitPullButton.tsx index e2d73fcbf3..5011b33b8f 100644 --- a/app/react/common/stacks/GitPullButton.tsx +++ b/app/react/common/stacks/GitPullButton.tsx @@ -51,8 +51,6 @@ export function GitPullButton({ stack }: { stack: Stack }) { Prune: stack.Option?.Prune, RepositoryAuthorizationType: stack.GitConfig?.Authentication?.AuthorizationType, - RepositoryGitCredentialID: - stack.GitConfig?.Authentication?.GitCredentialID, RepositoryUsername: stack.GitConfig?.Authentication?.Username, }, { diff --git a/app/react/common/stacks/queries/useCreateStack/createKubernetesStackFromGit.ts b/app/react/common/stacks/queries/useCreateStack/createKubernetesStackFromGit.ts index 0a8325b1d8..8adf63ade9 100644 --- a/app/react/common/stacks/queries/useCreateStack/createKubernetesStackFromGit.ts +++ b/app/react/common/stacks/queries/useCreateStack/createKubernetesStackFromGit.ts @@ -22,8 +22,6 @@ export type KubernetesGitRepositoryPayload = { repositoryUsername?: string; /** Password used in basic authentication. Required when RepositoryAuthentication is true. */ repositoryPassword?: string; - /** GitCredentialID used to identify the binded git credential */ - repositoryGitCredentialId?: number; /** Path to the Stack file inside the Git repository */ manifestFile?: string; diff --git a/app/react/common/stacks/queries/useCreateStack/createStandaloneStackFromGit.ts b/app/react/common/stacks/queries/useCreateStack/createStandaloneStackFromGit.ts index c1a59cb07e..660f648566 100644 --- a/app/react/common/stacks/queries/useCreateStack/createStandaloneStackFromGit.ts +++ b/app/react/common/stacks/queries/useCreateStack/createStandaloneStackFromGit.ts @@ -26,8 +26,6 @@ export type StandaloneGitRepositoryPayload = { repositoryUsername?: string; /** Password used in basic authentication. Required when RepositoryAuthentication is true. */ repositoryPassword?: string; - /** GitCredentialID used to identify the binded git credential */ - repositoryGitCredentialId?: number; /** Path to the Stack file inside the Git repository */ composeFile?: string; diff --git a/app/react/common/stacks/queries/useCreateStack/createSwarmStackFromGit.ts b/app/react/common/stacks/queries/useCreateStack/createSwarmStackFromGit.ts index 0ec9e5b0ef..53ac5f95c8 100644 --- a/app/react/common/stacks/queries/useCreateStack/createSwarmStackFromGit.ts +++ b/app/react/common/stacks/queries/useCreateStack/createSwarmStackFromGit.ts @@ -28,8 +28,6 @@ export type SwarmGitRepositoryPayload = { repositoryUsername?: string; /** Password used in basic authentication. Required when RepositoryAuthentication is true. */ repositoryPassword?: string; - /** GitCredentialID used to identify the binded git credential */ - repositoryGitCredentialId?: number; /** Path to the Stack file inside the Git repository */ composeFile?: string; diff --git a/app/react/common/stacks/queries/useCreateStack/useCreateStack.ts b/app/react/common/stacks/queries/useCreateStack/useCreateStack.ts index c4623de576..8b83274875 100644 --- a/app/react/common/stacks/queries/useCreateStack/useCreateStack.ts +++ b/app/react/common/stacks/queries/useCreateStack/useCreateStack.ts @@ -199,7 +199,6 @@ function createSwarmStack({ method, payload }: SwarmCreatePayload) { repositoryAuthentication: payload.git.RepositoryAuthentication, repositoryUsername: payload.git.RepositoryUsername, repositoryPassword: payload.git.RepositoryPassword, - repositoryGitCredentialId: payload.git.RepositoryGitCredentialID, filesystemPath: payload.relativePathSettings?.FilesystemPath, supportRelativePath: payload.relativePathSettings?.SupportRelativePath, tlsSkipVerify: payload.git.TLSSkipVerify, @@ -250,7 +249,6 @@ function createStandaloneStack({ method, payload }: StandaloneCreatePayload) { repositoryAuthentication: payload.git.RepositoryAuthentication, repositoryUsername: payload.git.RepositoryUsername, repositoryPassword: payload.git.RepositoryPassword, - repositoryGitCredentialId: payload.git.RepositoryGitCredentialID, filesystemPath: payload.relativePathSettings?.FilesystemPath, supportRelativePath: payload.relativePathSettings?.SupportRelativePath, tlsSkipVerify: payload.git.TLSSkipVerify, @@ -299,7 +297,6 @@ function createKubernetesStack({ method, payload }: KubernetesCreatePayload) { repositoryAuthentication: payload.git.RepositoryAuthentication, repositoryUsername: payload.git.RepositoryUsername, repositoryPassword: payload.git.RepositoryPassword, - repositoryGitCredentialId: payload.git.RepositoryGitCredentialID, tlsSkipVerify: payload.git.TLSSkipVerify, autoUpdate: transformAutoUpdateViewModel( diff --git a/app/react/common/stacks/types.ts b/app/react/common/stacks/types.ts index 99769a0c87..2c135aa6a7 100644 --- a/app/react/common/stacks/types.ts +++ b/app/react/common/stacks/types.ts @@ -102,7 +102,6 @@ export interface GitStackPayload { prune?: boolean; RepositoryReferenceName?: string; RepositoryAuthentication?: boolean; - RepositoryGitCredentialID?: number; RepositoryUsername?: string; RepositoryPassword?: string; RepositoryAuthorizationType?: AuthTypeOption; diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx b/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx index 571c2e2032..8df8ebf2d5 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx +++ b/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx @@ -102,7 +102,6 @@ describe('CreateStackForm - Webhook ID Integration', () => { RepositoryAuthentication: false, RepositoryUsername: '', RepositoryPassword: '', - RepositoryGitCredentialID: 0, TLSSkipVerify: false, AdditionalFiles: [], AutoUpdate: { @@ -115,8 +114,6 @@ describe('CreateStackForm - Webhook ID Integration', () => { RepositoryAuthorizationType: undefined, SupportRelativePath: false, FilesystemPath: '', - SaveCredential: false, - NewCredentialName: '', }, }), }); diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/GitSection.test.tsx b/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/GitSection.test.tsx index c12c023a64..4eb2959d31 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/GitSection.test.tsx +++ b/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/GitSection.test.tsx @@ -82,15 +82,12 @@ function renderComponent({ RepositoryAuthentication: false, RepositoryUsername: '', RepositoryPassword: '', - RepositoryGitCredentialID: 0, TLSSkipVerify: false, AdditionalFiles: [], AutoUpdate: undefined, RepositoryAuthorizationType: undefined, SupportRelativePath: false, FilesystemPath: '', - SaveCredential: false, - NewCredentialName: '', ...initialValues, }, }); diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.test.ts b/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.test.ts index 30afcc3f18..16534e0bc7 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.test.ts +++ b/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.test.ts @@ -3,9 +3,7 @@ import { getGitValidationSchema } from './validation'; describe('Git validation', () => { it('should pass validation with valid git form values', async () => { - const schema = getGitValidationSchema({ - gitCredentials: [], - }); + const schema = getGitValidationSchema(); const validData: GitFormValues = { RepositoryURL: 'https://github.com/user/repo', @@ -14,24 +12,19 @@ describe('Git validation', () => { RepositoryAuthentication: false, RepositoryUsername: '', RepositoryPassword: '', - RepositoryGitCredentialID: 0, TLSSkipVerify: false, AdditionalFiles: [], AutoUpdate: undefined, RepositoryAuthorizationType: undefined, SupportRelativePath: false, FilesystemPath: '', - SaveCredential: false, - NewCredentialName: '', }; await expect(schema.validate(validData)).resolves.toBeDefined(); }); it('should fail validation when repository URL is empty', async () => { - const schema = getGitValidationSchema({ - gitCredentials: [], - }); + const schema = getGitValidationSchema(); const invalidData = { RepositoryURL: '', @@ -41,30 +34,4 @@ describe('Git validation', () => { await expect(schema.validate(invalidData)).rejects.toThrow(); }); - - it('should require credential name when saveCredential is true', async () => { - const schema = getGitValidationSchema({ - gitCredentials: [], - }); - - const invalidData: GitFormValues = { - RepositoryURL: 'https://github.com/user/repo', - RepositoryReferenceName: 'refs/heads/main', - ComposeFilePathInRepository: 'docker-compose.yml', - RepositoryAuthentication: true, - RepositoryUsername: 'user', - RepositoryPassword: 'pass', - RepositoryGitCredentialID: 0, - TLSSkipVerify: false, - AdditionalFiles: [], - AutoUpdate: undefined, - RepositoryAuthorizationType: undefined, - SupportRelativePath: false, - FilesystemPath: '', - SaveCredential: true, - NewCredentialName: '', - }; - - await expect(schema.validate(invalidData)).rejects.toThrow(); - }); }); diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.ts b/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.ts index be79bfc627..b9c47e95b7 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.ts +++ b/app/react/docker/stacks/CreateView/CreateStackForm/GitSection/validation.ts @@ -1,16 +1,11 @@ import { SchemaOf, object, boolean, string } from 'yup'; -import { GitCredential } from '@/react/portainer/account/git-credentials/types'; import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm'; import { GitFormValues } from './types'; -export function getGitValidationSchema({ - gitCredentials = [], -}: { - gitCredentials: Array | undefined; -}): SchemaOf { - return buildGitValidationSchema(gitCredentials, false, 'compose').concat( +export function getGitValidationSchema(): SchemaOf { + return buildGitValidationSchema(false, 'compose').concat( object({ SupportRelativePath: boolean().default(false), FilesystemPath: string() diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/test-utils.ts b/app/react/docker/stacks/CreateView/CreateStackForm/test-utils.ts index a71ff93073..67fe33993d 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/test-utils.ts +++ b/app/react/docker/stacks/CreateView/CreateStackForm/test-utils.ts @@ -27,15 +27,12 @@ export function mockFormValues(overrides: DeepPartial): FormValues { RepositoryAuthentication: false, RepositoryUsername: '', RepositoryPassword: '', - RepositoryGitCredentialID: 0, TLSSkipVerify: false, AdditionalFiles: [], AutoUpdate: undefined, RepositoryAuthorizationType: undefined, SupportRelativePath: false, FilesystemPath: '', - SaveCredential: false, - NewCredentialName: '', }, template: { selectedId: undefined, diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/useValidationSchema.ts b/app/react/docker/stacks/CreateView/CreateStackForm/useValidationSchema.ts index 247a3e1c20..f6b2ba1cd4 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/useValidationSchema.ts +++ b/app/react/docker/stacks/CreateView/CreateStackForm/useValidationSchema.ts @@ -4,13 +4,11 @@ import _ from 'lodash'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { useStacks } from '@/react/common/stacks/queries/useStacks'; import { useContainers } from '@/react/docker/containers/queries/useContainers'; -import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser'; +import { useIsEdgeAdmin } from '@/react/hooks/useUser'; import { getValidationSchema } from './validation'; export function useValidationSchema(environmentId: EnvironmentId) { - const { user } = useCurrentUser(); const { isAdmin } = useIsEdgeAdmin(); const stacksQuery = useStacks(); @@ -18,11 +16,9 @@ export function useValidationSchema(environmentId: EnvironmentId) { select: (containers) => containers.flatMap((c) => c.Names).map((name) => _.trimStart(name, '/')), }); - const gitCredentialsQuery = useGitCredentials(user.Id); const containerNames = containersQuery.data; const stacks = stacksQuery.data; - const gitCredentials = gitCredentialsQuery.data; return useMemo( () => @@ -31,8 +27,7 @@ export function useValidationSchema(environmentId: EnvironmentId) { environmentId, stacks, containerNames, - gitCredentials, }), - [isAdmin, environmentId, stacks, containerNames, gitCredentials] + [isAdmin, environmentId, stacks, containerNames] ); } diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/validation.ts b/app/react/docker/stacks/CreateView/CreateStackForm/validation.ts index 1bb99fc88d..9fc1708528 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/validation.ts +++ b/app/react/docker/stacks/CreateView/CreateStackForm/validation.ts @@ -1,7 +1,6 @@ import { object, array, number, mixed, SchemaOf, bool } from 'yup'; import { accessControlFormValidation } from '@/react/portainer/access-control/AccessControlForm'; -import { GitCredential } from '@/react/portainer/account/git-credentials/types'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { nameValidation } from '@/react/docker/stacks/common/NameField'; import { Stack } from '@/react/common/stacks/types'; @@ -19,17 +18,15 @@ export function getValidationSchema({ environmentId, stacks, containerNames = [], - gitCredentials = [], }: { isAdmin: boolean; environmentId: EnvironmentId; stacks?: Array; containerNames?: Array; - gitCredentials?: Array; }): SchemaOf { return getBaseValidationSchema({ isAdmin, environmentId, stacks }).concat( object({ - git: getGitValidationSchema({ gitCredentials }).when('method', { + git: getGitValidationSchema().when('method', { is: 'repository', then: (schema) => schema.required(), otherwise: () => mixed(), diff --git a/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts b/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts index f6b6ae64e9..3e7c04faf1 100644 --- a/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts +++ b/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts @@ -12,8 +12,6 @@ import { useMemo } from 'react'; import Lazy from 'yup/lib/Lazy'; import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm'; -import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { useCurrentUser } from '@/react/hooks/useUser'; import { relativePathValidation } from '@/react/portainer/gitops/RelativePathFieldset/validation'; import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types'; import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model'; @@ -43,8 +41,6 @@ export function useValidation({ appTemplate: TemplateViewModel | undefined; customTemplate: CustomTemplate | undefined; }): Lazy> { - const { user } = useCurrentUser(); - const gitCredentialsQuery = useGitCredentials(user.Id); const nameValidation = useNameValidation(); const edgeGroupsQuery = useEdgeGroups(); const edgeGroups = edgeGroupsQuery.data; @@ -131,7 +127,6 @@ export function useValidation({ ? 'compose' : 'manifest'; return buildGitValidationSchema( - gitCredentialsQuery.data || [], !!customTemplate, deploymentMethod ); @@ -144,12 +139,6 @@ export function useValidation({ useManifestNamespaces: boolean().default(false), }) ), - [ - appTemplate?.Env, - customTemplate, - edgeGroups, - gitCredentialsQuery.data, - nameValidation, - ] + [appTemplate?.Env, customTemplate, edgeGroups, nameValidation] ); } diff --git a/app/react/edge/edge-stacks/CreateView/tests/custom-templates.test.tsx b/app/react/edge/edge-stacks/CreateView/tests/custom-templates.test.tsx index 7de3eb00f3..ee7bbae986 100644 --- a/app/react/edge/edge-stacks/CreateView/tests/custom-templates.test.tsx +++ b/app/react/edge/edge-stacks/CreateView/tests/custom-templates.test.tsx @@ -52,7 +52,6 @@ const expectedCustomTemplatePayload = { repositoryReferenceName: 'refs/heads/main', filePathInRepository: 'docker/voting.yaml', repositoryAuthentication: false, - repositoryGitCredentialId: 0, repositoryPassword: '', filesystemPath: '/test', supportRelativePath: true, diff --git a/app/react/edge/edge-stacks/CreateView/tests/utils.test.tsx b/app/react/edge/edge-stacks/CreateView/tests/utils.test.tsx index 6af7868cd3..11dfb6cd9e 100644 --- a/app/react/edge/edge-stacks/CreateView/tests/utils.test.tsx +++ b/app/react/edge/edge-stacks/CreateView/tests/utils.test.tsx @@ -85,7 +85,6 @@ const customTemplatesResponseBody = [ Authentication: { Username: '', Password: '', - GitCredentialID: 0, }, ConfigHash: '1db40a888e07da7d9455897aadd349d0bc83bd83', TLSSkipVerify: false, @@ -145,16 +144,6 @@ const edgeGroups = [ }, ]; -const gitCredentials = [ - { - id: 1, - userId: 1, - name: 'test', - username: 'portainer-test', - creationDate: 1732761658, - }, -]; - const registries = [ { Id: 1, @@ -238,11 +227,6 @@ export function renderCreateForm() { server.use(http.get('/api/edge_stacks', () => HttpResponse.json([]))); server.use(http.get('/api/edge_groups', () => HttpResponse.json(edgeGroups))); server.use(http.get('/api/registries', () => HttpResponse.json(registries))); - server.use( - http.get('/api/users/1/gitcredentials', () => - HttpResponse.json(gitCredentials) - ) - ); const Wrapped = withTestQueryProvider( withUserProvider(withTestRouter(CreateForm), user) ); diff --git a/app/react/edge/edge-stacks/CreateView/useCreate.tsx b/app/react/edge/edge-stacks/CreateView/useCreate.tsx index b4781f5a88..b7f2361ce7 100644 --- a/app/react/edge/edge-stacks/CreateView/useCreate.tsx +++ b/app/react/edge/edge-stacks/CreateView/useCreate.tsx @@ -1,6 +1,5 @@ import { useRouter } from '@uirouter/react'; -import { useCurrentUser } from '@/react/hooks/useUser'; import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model'; import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types'; import { notifySuccess } from '@/portainer/services/notifications'; @@ -26,7 +25,6 @@ export function useCreate({ }) { const router = useRouter(); const mutation = useCreateEdgeStack(); - const { user } = useCurrentUser(); return { isLoading: mutation.isLoading, @@ -105,7 +103,6 @@ export function useCreate({ value, })); return { - userId: user.Id, deploymentType: values.deploymentType, edgeGroups: values.groupIds, name: values.name, diff --git a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.test.tsx b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.test.tsx index 54cf4656ac..8f00220ebb 100644 --- a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.test.tsx +++ b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.test.tsx @@ -46,7 +46,6 @@ describe('GitForm', () => { Authentication: { Username: '', Password: '', - RepositoryGitCredentialID: 0, }, }, PrePullImage: false, diff --git a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx index 85217965cf..18fd237918 100644 --- a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx +++ b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx @@ -33,7 +33,6 @@ import { Registry } from '@/react/portainer/registries/types/registry'; import { useRegistries } from '@/react/portainer/registries/queries/useRegistries'; import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset'; import { parseRelativePathResponse } from '@/react/portainer/gitops/RelativePathFieldset/utils'; -import { useSaveCredentialsIfRequired } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation'; import { GitReferenceCard } from '@/react/portainer/gitops/GitReferenceCard'; import { LoadingButton } from '@@/buttons'; @@ -65,8 +64,6 @@ interface FormValues { export function GitForm({ stack }: { stack: EdgeStack }) { const router = useRouter(); const updateStackMutation = useUpdateEdgeStackGitMutation(); - const { saveCredentials, isLoading: isSaveCredentialsLoading } = - useSaveCredentialsIfRequired(); const [webhookId] = useState( () => stack.AutoUpdate?.Webhook || createWebhookId() @@ -96,9 +93,7 @@ export function GitForm({ stack }: { stack: EdgeStack }) { webhookId={webhookId} onUpdateSettingsClick={handleUpdateSettings} gitUrl={gitConfig.URL} - isLoading={ - updateStackMutation.isLoading || isSaveCredentialsLoading - } + isLoading={updateStackMutation.isLoading} isUpdateVersion={!!updateStackMutation.variables?.updateVersion} stack={stack} /> @@ -109,9 +104,7 @@ export function GitForm({ stack }: { stack: EdgeStack }) { return; } - const credentialId = await saveCredentials(values.authentication); - - updateStackMutation.mutate(getPayload(values, credentialId, false), { + updateStackMutation.mutate(getPayload(values, false), { onSuccess() { notifySuccess('Success', 'Stack updated successfully'); router.stateService.reload(); @@ -123,9 +116,7 @@ export function GitForm({ stack }: { stack: EdgeStack }) { ); async function handleSubmit(values: FormValues) { - const credentialId = await saveCredentials(values.authentication); - - updateStackMutation.mutate(getPayload(values, credentialId, true), { + updateStackMutation.mutate(getPayload(values, true), { onSuccess() { notifySuccess('Success', 'Stack updated successfully'); router.stateService.reload(); @@ -135,16 +126,12 @@ export function GitForm({ stack }: { stack: EdgeStack }) { function getPayload( { authentication, autoUpdate, privateRegistryId, ...values }: FormValues, - credentialId: number | undefined, updateVersion: boolean ): UpdateEdgeStackGitPayload { return { updateVersion, id: stack.Id, - authentication: transformGitAuthenticationViewModel({ - ...authentication, - RepositoryGitCredentialID: credentialId, - }), + authentication: transformGitAuthenticationViewModel(authentication), autoUpdate: transformAutoUpdateViewModel(autoUpdate, webhookId), registries: typeof privateRegistryId !== 'undefined' diff --git a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts index bc6f4472ef..a1fa29530c 100644 --- a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts +++ b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/createStackFromGit.ts @@ -23,8 +23,6 @@ export type GitRepositoryPayload = { repositoryUsername?: string; /** Password used in basic authentication. Required when RepositoryAuthentication is true. */ repositoryPassword?: string; - /** GitCredentialID used to identify the binded git credential */ - repositoryGitCredentialId?: number; /** Path to the Stack file inside the Git repository */ filePathInRepository?: string; /** List of identifiers of EdgeGroups */ diff --git a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts index 65b47f0ea4..1e33a44f10 100644 --- a/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts +++ b/app/react/edge/edge-stacks/queries/useCreateEdgeStack/useCreateEdgeStack.ts @@ -8,8 +8,6 @@ import { GitFormModel, RelativePathModel, } from '@/react/portainer/gitops/types'; -import { saveGitCredentialsIfNeeded } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation'; -import { UserId } from '@/portainer/users/types'; import { DeploymentType, StaggerConfig } from '../../types'; @@ -22,7 +20,6 @@ export function useCreateEdgeStack() { } export type BasePayload = { - userId: UserId; /** Name of the stack */ name: string; /** Content of the Stack file */ @@ -92,7 +89,7 @@ function createEdgeStack({ method, payload }: CreateEdgeStackPayload) { Webhook: payload.webhook, }); case 'git': - return createStackAndGitCredential(payload.userId, payload); + return createEdgeStackFromGit(payload); case 'string': return createStackFromFileContent({ deploymentType: payload.deploymentType, @@ -112,16 +109,13 @@ function createEdgeStack({ method, payload }: CreateEdgeStackPayload) { } } -async function createStackAndGitCredential( - userId: UserId, +function createEdgeStackFromGit( payload: BasePayload & { git: GitFormModel; relativePathSettings?: RelativePathModel; autoUpdate: AutoUpdateResponse | null; } ) { - const resolvedAuth = await saveGitCredentialsIfNeeded(userId, payload.git); - return createStackFromGit({ deploymentType: payload.deploymentType, edgeGroups: payload.edgeGroups, @@ -135,10 +129,9 @@ async function createStackAndGitCredential( repositoryUrl: payload.git.RepositoryURL, repositoryReferenceName: payload.git.RepositoryReferenceName, filePathInRepository: payload.git.ComposeFilePathInRepository, - repositoryAuthentication: resolvedAuth.RepositoryAuthentication, - repositoryUsername: resolvedAuth.RepositoryUsername, - repositoryPassword: resolvedAuth.RepositoryPassword, - repositoryGitCredentialId: resolvedAuth.RepositoryGitCredentialID, + repositoryAuthentication: payload.git.RepositoryAuthentication, + repositoryUsername: payload.git.RepositoryUsername, + repositoryPassword: payload.git.RepositoryPassword, filesystemPath: payload.relativePathSettings?.FilesystemPath, supportRelativePath: payload.relativePathSettings?.SupportRelativePath, perDeviceConfigsGroupMatchType: diff --git a/app/react/portainer/account/git-credentials/git-credentials.service.ts b/app/react/portainer/account/git-credentials/git-credentials.service.ts deleted file mode 100644 index c330d1aa3d..0000000000 --- a/app/react/portainer/account/git-credentials/git-credentials.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; - -import axios, { parseAxiosError } from '@/portainer/services/axios/axios'; -import { success as notifySuccess } from '@/portainer/services/notifications'; -import { UserId } from '@/portainer/users/types'; - -import { isBE } from '../../feature-flags/feature-flags.service'; - -import { GitCredential, UpdateGitCredentialPayload } from './types'; - -export async function getGitCredentials(userId: number) { - try { - const { data } = await axios.get(buildGitUrl(userId)); - return data; - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to get git credentials'); - } -} - -export async function getGitCredential(userId: number, id: number) { - try { - const { data } = await axios.get(buildGitUrl(userId, id)); - return data; - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to get git credential'); - } -} - -export async function deleteGitCredential(credential: GitCredential) { - try { - await axios.delete( - buildGitUrl(credential.userId, credential.id) - ); - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to delete git credential'); - } -} - -export async function updateGitCredential( - credential: Partial, - userId: number, - id: number -) { - try { - const { data } = await axios.put(buildGitUrl(userId, id), credential); - return data; - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to update credential'); - } -} - -export function useUpdateGitCredentialMutation() { - const queryClient = useQueryClient(); - - return useMutation( - ({ - credential, - userId, - id, - }: { - credential: UpdateGitCredentialPayload; - userId: number; - id: number; - }) => updateGitCredential(credential, userId, id), - { - onSuccess: (_, data) => { - notifySuccess( - 'Git credential updated successfully', - data.credential.name - ); - return queryClient.invalidateQueries(['gitcredentials']); - }, - meta: { - error: { - title: 'Failure', - message: 'Unable to update credential', - }, - }, - } - ); -} - -export function useDeleteGitCredentialMutation() { - const queryClient = useQueryClient(); - - return useMutation(deleteGitCredential, { - onSuccess: (_, credential) => { - notifySuccess('Git Credential deleted successfully', credential.name); - return queryClient.invalidateQueries(['gitcredentials']); - }, - meta: { - error: { - title: 'Failure', - message: 'Unable to delete git credential', - }, - }, - }); -} - -export function useGitCredentials( - userId: UserId, - { enabled }: { enabled?: boolean } = {} -) { - return useQuery(['gitcredentials'], () => getGitCredentials(userId), { - enabled: isBE && enabled, - meta: { - error: { - title: 'Failure', - message: 'Unable to retrieve git credentials', - }, - }, - }); -} - -export function useGitCredential(userId: number, id: number) { - return useQuery(['gitcredentials', id], () => getGitCredential(userId, id), { - meta: { - error: { - title: 'Failure', - message: 'Unable to retrieve git credential', - }, - }, - }); -} - -export function buildGitUrl(userId: number, credentialId?: number) { - return credentialId - ? `/users/${userId}/gitcredentials/${credentialId}` - : `/users/${userId}/gitcredentials`; -} diff --git a/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts b/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts deleted file mode 100644 index 2f44ded4f1..0000000000 --- a/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { useQueryClient, useMutation } from '@tanstack/react-query'; - -import axios, { parseAxiosError } from '@/portainer/services/axios/axios'; -import { notifyError, notifySuccess } from '@/portainer/services/notifications'; -import { - GitAuthModel, - GitCredentialsModel, -} from '@/react/portainer/gitops/types'; -import { useCurrentUser } from '@/react/hooks/useUser'; -import { UserId } from '@/portainer/users/types'; - -import { GitCredential } from '../types'; -import { buildGitUrl } from '../git-credentials.service'; - -export interface CreateGitCredentialPayload { - userId: number; - name: string; - username?: string; - password: string; -} - -export function useCreateGitCredentialMutation() { - const queryClient = useQueryClient(); - - return useMutation(createGitCredential, { - onSuccess: (_, payload) => { - notifySuccess('Credentials created successfully', payload.name); - return queryClient.invalidateQueries(['gitcredentials']); - }, - meta: { - error: { - title: 'Failure', - message: 'Unable to create credential', - }, - }, - }); -} - -async function createGitCredential(gitCredential: CreateGitCredentialPayload) { - try { - const { data } = await axios.post<{ gitCredential: GitCredential }>( - buildGitUrl(gitCredential.userId), - gitCredential - ); - return data.gitCredential; - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to create git credential'); - } -} - -export function useSaveCredentialsIfRequired() { - const saveCredentialsMutation = useCreateGitCredentialMutation(); - const { user } = useCurrentUser(); - - return { - saveCredentials: saveCredentialsIfRequired, - isLoading: saveCredentialsMutation.isLoading, - }; - - async function saveCredentialsIfRequired(authentication?: GitAuthModel) { - if (!authentication) { - return undefined; - } - - if ( - !authentication.SaveCredential || - !authentication.RepositoryPassword || - !authentication.NewCredentialName - ) { - return authentication.RepositoryGitCredentialID; - } - - try { - const credential = await saveCredentialsMutation.mutateAsync({ - userId: user.Id, - username: authentication.RepositoryUsername, - password: authentication.RepositoryPassword, - name: authentication.NewCredentialName, - }); - return credential.id; - } catch (err) { - notifyError('Error', err as Error, 'Unable to save credentials'); - return undefined; - } - } -} - -export async function saveGitCredentialsIfNeeded( - userId: UserId, - gitModel: GitAuthModel -): Promise { - let credentialsId = gitModel.RepositoryGitCredentialID; - let username = gitModel.RepositoryUsername; - let password = gitModel.RepositoryPassword; - if ( - gitModel.SaveCredential && - gitModel.RepositoryAuthentication && - password && - username && - gitModel.NewCredentialName - ) { - const cred = await createGitCredential({ - name: gitModel.NewCredentialName, - password, - username, - userId, - }); - credentialsId = cred.id; - } - - // clear username and password if credentials are provided - if (credentialsId && username) { - username = ''; - password = ''; - } - - return { - RepositoryAuthentication: gitModel.RepositoryAuthentication, - RepositoryGitCredentialID: credentialsId, - RepositoryUsername: username, - RepositoryPassword: password, - RepositoryAuthorizationType: gitModel.RepositoryAuthorizationType, - }; -} diff --git a/app/react/portainer/account/git-credentials/types.ts b/app/react/portainer/account/git-credentials/types.ts index f48bbf3ca0..d1f2e0ed9f 100644 --- a/app/react/portainer/account/git-credentials/types.ts +++ b/app/react/portainer/account/git-credentials/types.ts @@ -1,33 +1,4 @@ -import { - PaginationTableSettings, - SortableTableSettings, -} from '@@/datatables/types'; - -export interface GitCredentialTableSettings - extends SortableTableSettings, PaginationTableSettings {} - export enum AuthTypeOption { Basic = 0, Token = 1, } - -export interface GitCredentialFormValues { - name: string; - username?: string; - password: string; -} - -export interface UpdateGitCredentialPayload { - name: string; - username?: string; - password: string; -} - -export type GitCredential = { - id: number; - userId: number; - name: string; - username: string; - creationDate: number; - authorizationType: AuthTypeOption; -}; diff --git a/app/react/portainer/generated-api/portainer/types.gen.ts b/app/react/portainer/generated-api/portainer/types.gen.ts index 5515e10928..687b0edaf9 100644 --- a/app/react/portainer/generated-api/portainer/types.gen.ts +++ b/app/react/portainer/generated-api/portainer/types.gen.ts @@ -96,12 +96,6 @@ export type WorkflowsDeploymentPlatform = export type GittypesGitAuthentication = { AuthorizationType?: number; - /** - * Git credentials identifier when the value is not 0 - * When the value is 0, Username and Password are set without using saved credential - * This is introduced since 2.15.0 - */ - GitCredentialID?: number; Password?: string; Provider?: number; Username?: string; @@ -7844,14 +7838,9 @@ export type CustomtemplatesCustomTemplateUpdatePayload = { * Use authentication to clone the Git repository */ RepositoryAuthentication?: boolean; - /** - * GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication - * is true and RepositoryUsername/RepositoryPassword are not provided - */ - RepositoryGitCredentialID?: number; /** * Password used in basic authentication or token used in token authentication. - * Required when RepositoryAuthentication is true and RepositoryGitCredentialID is 0 + * Required when RepositoryAuthentication is true */ RepositoryPassword?: string; /** @@ -7864,7 +7853,6 @@ export type CustomtemplatesCustomTemplateUpdatePayload = { RepositoryURL: string; /** * Username used in basic authentication. Required when RepositoryAuthentication is true - * and RepositoryGitCredentialID is 0. Ignored if RepositoryAuthType is token */ RepositoryUsername?: string; /** diff --git a/app/react/portainer/generated-api/portainer/zod.gen.ts b/app/react/portainer/generated-api/portainer/zod.gen.ts index 0e2369365a..6d7bd4bfab 100644 --- a/app/react/portainer/generated-api/portainer/zod.gen.ts +++ b/app/react/portainer/generated-api/portainer/zod.gen.ts @@ -87,7 +87,6 @@ export const zWorkflowsDeploymentPlatform = z.enum(WorkflowsDeploymentPlatform); export const zGittypesGitAuthentication = z.object({ AuthorizationType: z.int().optional(), - GitCredentialID: z.int().optional(), Password: z.string().optional(), Provider: z.int().optional(), Username: z.string().optional(), @@ -2986,7 +2985,6 @@ export const zCustomtemplatesCustomTemplateUpdatePayload = z.object({ Note: z.string().optional(), Platform: z.union([z.literal(1), z.literal(2)]).optional(), RepositoryAuthentication: z.boolean().optional(), - RepositoryGitCredentialID: z.int().optional(), RepositoryPassword: z.string().optional(), RepositoryReferenceName: z.string().optional(), RepositoryURL: z.string(), diff --git a/app/react/portainer/gitops/AuthFieldset/AuthFieldset.test.tsx b/app/react/portainer/gitops/AuthFieldset/AuthFieldset.test.tsx index c4cc8d7137..6b1a62d476 100644 --- a/app/react/portainer/gitops/AuthFieldset/AuthFieldset.test.tsx +++ b/app/react/portainer/gitops/AuthFieldset/AuthFieldset.test.tsx @@ -1,15 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; -import { - GitCredential, - AuthTypeOption, -} from '@/react/portainer/account/git-credentials/types'; +import { AuthTypeOption } from '@/react/portainer/account/git-credentials/types'; import { GitAuthModel } from '@/react/portainer/gitops/types'; import { AuthFieldset, gitAuthValidation } from './AuthFieldset'; -// Simple mocks to avoid complex dependencies vi.mock('../../feature-flags/feature-flags.service', () => ({ isBE: true, isLimitedToBE: () => false, @@ -22,26 +18,11 @@ vi.mock('@/react/hooks/useDebounce', () => ({ ], })); -vi.mock('./CredentialSelector', () => ({ - CredentialSelector: () => ( -
Credential Selector
- ), -})); - -vi.mock('./NewCredentialForm', () => ({ - NewCredentialForm: () => ( -
New Credential Form
- ), -})); - const defaultGitAuthModel: GitAuthModel = { RepositoryAuthentication: false, - RepositoryGitCredentialID: 0, RepositoryUsername: '', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: false, - NewCredentialName: '', }; function renderAuthFieldset({ @@ -97,26 +78,6 @@ describe('AuthFieldset', () => { ).toBeInTheDocument(); }); - it('should render credential selector when authentication is enabled', () => { - renderAuthFieldset({ - value: { ...defaultGitAuthModel, RepositoryAuthentication: true }, - }); - - expect(screen.getByTestId('credential-selector')).toBeInTheDocument(); - }); - - it('should render new credential form when password is provided', () => { - renderAuthFieldset({ - value: { - ...defaultGitAuthModel, - RepositoryAuthentication: true, - RepositoryPassword: 'password123', - }, - }); - - expect(screen.getByTestId('new-credential-form')).toBeInTheDocument(); - }); - it('should not render interactive fields when authentication is disabled', () => { renderAuthFieldset({ value: { ...defaultGitAuthModel, RepositoryAuthentication: false }, @@ -128,9 +89,6 @@ describe('AuthFieldset', () => { expect( screen.queryByTestId('component-gitPasswordInput') ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('credential-selector') - ).not.toBeInTheDocument(); }); }); @@ -220,12 +178,9 @@ describe('AuthFieldset', () => { it('should handle value prop with all fields populated', () => { const value: GitAuthModel = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'testuser', RepositoryPassword: 'testpass', RepositoryAuthorizationType: AuthTypeOption.Token, - SaveCredential: true, - NewCredentialName: 'test-credential', }; renderAuthFieldset({ value }); @@ -236,73 +191,30 @@ describe('AuthFieldset', () => { expect( screen.getByTestId('component-gitPasswordInput') ).toBeInTheDocument(); - expect(screen.getByTestId('credential-selector')).toBeInTheDocument(); - expect(screen.getByTestId('new-credential-form')).toBeInTheDocument(); - }); - - it('should handle value prop with git credential selected', () => { - const value: GitAuthModel = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: false, - NewCredentialName: '', - }; - - renderAuthFieldset({ value }); - - expect(screen.getByTestId('credential-selector')).toBeInTheDocument(); - // shouldn't render credential inputs for selected credential - expect( - screen.queryByTestId('component-gitUsernameInput') - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('component-gitPasswordInput') - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('new-credential-form') - ).not.toBeInTheDocument(); }); }); }); describe('gitAuthValidation', () => { - const mockGitCredentials: GitCredential[] = [ - { - id: 1, - userId: 1, - name: 'existing-credential', - username: 'testuser', - creationDate: Date.now(), - authorizationType: AuthTypeOption.Basic, - }, - ]; - describe('default values', () => { it('should provide correct default values', async () => { - const schema = gitAuthValidation([], false, false); + const schema = gitAuthValidation(false, false); const result = await schema.validate({}); expect(result).toEqual({ RepositoryAuthentication: false, - RepositoryGitCredentialID: 0, RepositoryUsername: '', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: false, - NewCredentialName: '', }); }); }); describe('authentication disabled', () => { it('should allow empty values when authentication is disabled', async () => { - const schema = gitAuthValidation([], false, false); + const schema = gitAuthValidation(false, false); const data = { RepositoryAuthentication: false, - RepositoryGitCredentialID: 0, RepositoryUsername: '', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, @@ -313,12 +225,11 @@ describe('gitAuthValidation', () => { }); }); - describe('authentication enabled without git credential', () => { - it('should require username when authentication is enabled and no git credential is selected', async () => { - const schema = gitAuthValidation([], false, false); + describe('authentication enabled', () => { + it('should require username when authentication is enabled', async () => { + const schema = gitAuthValidation(false, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: '', RepositoryPassword: 'password', RepositoryAuthorizationType: AuthTypeOption.Basic, @@ -329,11 +240,10 @@ describe('gitAuthValidation', () => { ); }); - it('should require password when authentication is enabled, no git credential, not auth edit, and not from custom template', async () => { - const schema = gitAuthValidation([], false, false); + it('should require password when authentication is enabled, not auth edit, and not from custom template', async () => { + const schema = gitAuthValidation(false, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, @@ -344,26 +254,23 @@ describe('gitAuthValidation', () => { ); }); - it('should set default authorization type when authentication is enabled, no git credential, not auth edit, and not from custom template', async () => { - const schema = gitAuthValidation([], false, false); + it('should set default authorization type when authentication is enabled', async () => { + const schema = gitAuthValidation(false, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: 'password', RepositoryAuthorizationType: undefined, }; - // The schema provides a default value when authorization type is undefined const result = await schema.validate(data); expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); }); it('should accept valid authorization types', async () => { - const schema = gitAuthValidation([], false, false); + const schema = gitAuthValidation(false, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: 'password', RepositoryAuthorizationType: AuthTypeOption.Token, @@ -374,126 +281,12 @@ describe('gitAuthValidation', () => { }); it('should reject invalid authorization types', async () => { - const schema = gitAuthValidation([], false, false); + const schema = gitAuthValidation(false, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: 'password', - RepositoryAuthorizationType: 999, // Invalid value - }; - - await expect(schema.validate(data)).rejects.toThrow(); - }); - - it('should reject string authorization types that are not valid enum values', async () => { - const schema = gitAuthValidation([], false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: - 'invalid-auth-type' as unknown as AuthTypeOption, - }; - - await expect(schema.validate(data)).rejects.toThrow(); - }); - }); - - describe('authentication enabled with git credential', () => { - it('should not require username when git credential is selected', async () => { - const schema = gitAuthValidation([], false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: AuthTypeOption.Basic, - }; - - const result = await schema.validate(data); - expect(result.RepositoryUsername).toBe(''); - }); - - it('should not require password when git credential is selected', async () => { - const schema = gitAuthValidation([], false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: AuthTypeOption.Basic, - }; - - const result = await schema.validate(data); - expect(result.RepositoryPassword).toBe(''); - }); - - it('should not require authorization type when git credential is selected', async () => { - const schema = gitAuthValidation([], false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: undefined, - }; - - const result = await schema.validate(data); - expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); // Default value - }); - - it('should accept the authorization type from the selected git credential', async () => { - const schema = gitAuthValidation(mockGitCredentials, false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, // This matches the mockGitCredentials[0].id - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: AuthTypeOption.Basic, // This matches mockGitCredentials[0].authorizationType - }; - - const result = await schema.validate(data); - expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); - }); - - it('should not require authorization type validation when git credential is selected', async () => { - const schema = gitAuthValidation(mockGitCredentials, false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: undefined, // Should not be required when git credential is selected - }; - - const result = await schema.validate(data); - expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); // Should default to Basic - }); - - it('should reject invalid authorization type even when git credential is selected', async () => { - const schema = gitAuthValidation(mockGitCredentials, false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: 999, // Invalid value - should be rejected by oneOf validation - }; - - await expect(schema.validate(data)).rejects.toThrow(); - }); - - it('should reject string authorization type that is not valid enum value even when git credential is selected', async () => { - const schema = gitAuthValidation(mockGitCredentials, false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: - 'invalid-auth-type' as unknown as AuthTypeOption, // Invalid string value + RepositoryAuthorizationType: 999, }; await expect(schema.validate(data)).rejects.toThrow(); @@ -502,10 +295,9 @@ describe('gitAuthValidation', () => { describe('auth edit mode', () => { it('should not require password when in auth edit mode', async () => { - const schema = gitAuthValidation([], true, false); + const schema = gitAuthValidation(true, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, @@ -516,26 +308,24 @@ describe('gitAuthValidation', () => { }); it('should not require authorization type when in auth edit mode', async () => { - const schema = gitAuthValidation([], true, false); + const schema = gitAuthValidation(true, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: 'password', RepositoryAuthorizationType: undefined, }; const result = await schema.validate(data); - expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); // Default value + expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); }); }); describe('created from custom template', () => { it('should not require password when created from custom template', async () => { - const schema = gitAuthValidation([], false, true); + const schema = gitAuthValidation(false, true); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, @@ -546,218 +336,57 @@ describe('gitAuthValidation', () => { }); it('should not require authorization type when created from custom template', async () => { - const schema = gitAuthValidation([], false, true); + const schema = gitAuthValidation(false, true); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'username', RepositoryPassword: 'password', RepositoryAuthorizationType: undefined, }; const result = await schema.validate(data); - expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); // Default value - }); - }); - - describe('save credential validation', () => { - it('should not require new credential name when save credential is disabled', async () => { - const schema = gitAuthValidation([], false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: false, - NewCredentialName: '', - }; - - const result = await schema.validate(data); - expect(result.NewCredentialName).toBe(''); - }); - - it('should require new credential name when save credential is enabled and not in auth edit mode', async () => { - const schema = gitAuthValidation([], false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: '', - }; - - await expect(schema.validate(data)).rejects.toThrow('Name is required'); - }); - - it('should not require new credential name when save credential is enabled but in auth edit mode', async () => { - const schema = gitAuthValidation([], true, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: '', - }; - - const result = await schema.validate(data); - expect(result.NewCredentialName).toBe(''); - }); - - it('should reject duplicate credential names', async () => { - const schema = gitAuthValidation(mockGitCredentials, false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: 'existing-credential', - }; - - await expect(schema.validate(data)).rejects.toThrow( - 'This name is already been used, please try another one' - ); - }); - - it('should accept unique credential names', async () => { - const schema = gitAuthValidation(mockGitCredentials, false, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: 'new-credential', - }; - - const result = await schema.validate(data); - expect(result.NewCredentialName).toBe('new-credential'); - }); - - it('should validate credential name format - valid names', async () => { - const schema = gitAuthValidation([], false, false); - const validNames = ['my-name', 'abc-123', 'test_credential', 'simple123']; - - await Promise.all( - validNames.map(async (name) => { - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: name, - }; - - const result = await schema.validate(data); - expect(result.NewCredentialName).toBe(name); - }) - ); - }); - - it('should validate credential name format - invalid names', async () => { - const schema = gitAuthValidation([], false, false); - const invalidNames = [ - 'My-Name', - 'ABC-123', - 'test@credential', - 'simple 123', - 'test.credential', - ]; - - await Promise.all( - invalidNames.map(async (name) => { - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, - RepositoryUsername: 'username', - RepositoryPassword: 'password', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: name, - }; - - await expect(schema.validate(data)).rejects.toThrow( - "This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123')." - ); - }) - ); + expect(result.RepositoryAuthorizationType).toBe(AuthTypeOption.Basic); }); }); describe('complex scenarios', () => { - it('should handle complete valid data with save credential', async () => { - const schema = gitAuthValidation([], false, false); + it('should handle complete valid data', async () => { + const schema = gitAuthValidation(false, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'testuser', RepositoryPassword: 'testpassword', RepositoryAuthorizationType: AuthTypeOption.Token, - SaveCredential: true, - NewCredentialName: 'my-test-credential', }; const result = await schema.validate(data); expect(result).toEqual(data); }); - it('should handle complete valid data with git credential', async () => { - const schema = gitAuthValidation(mockGitCredentials, false, false); + it('should handle auth edit mode', async () => { + const schema = gitAuthValidation(true, false); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 1, - RepositoryUsername: '', - RepositoryPassword: '', - RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: false, - NewCredentialName: '', - }; - - const result = await schema.validate(data); - expect(result).toEqual(data); - }); - - it('should handle auth edit mode with save credential', async () => { - const schema = gitAuthValidation([], true, false); - const data = { - RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'testuser', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: '', }; const result = await schema.validate(data); expect(result.RepositoryPassword).toBe(''); - expect(result.NewCredentialName).toBe(''); }); - it('should handle custom template creation with save credential', async () => { - const schema = gitAuthValidation([], false, true); + it('should handle custom template creation', async () => { + const schema = gitAuthValidation(false, true); const data = { RepositoryAuthentication: true, - RepositoryGitCredentialID: 0, RepositoryUsername: 'testuser', RepositoryPassword: '', RepositoryAuthorizationType: AuthTypeOption.Basic, - SaveCredential: true, - NewCredentialName: 'template-credential', }; const result = await schema.validate(data); expect(result.RepositoryPassword).toBe(''); - expect(result.NewCredentialName).toBe('template-credential'); }); }); }); diff --git a/app/react/portainer/gitops/AuthFieldset/AuthFieldset.tsx b/app/react/portainer/gitops/AuthFieldset/AuthFieldset.tsx index eaf84186e2..7f53dc87b5 100644 --- a/app/react/portainer/gitops/AuthFieldset/AuthFieldset.tsx +++ b/app/react/portainer/gitops/AuthFieldset/AuthFieldset.tsx @@ -1,19 +1,15 @@ import { FormikErrors } from 'formik'; -import { boolean, mixed, number, object, SchemaOf, string } from 'yup'; +import { boolean, mixed, object, SchemaOf, string } from 'yup'; import { useState } from 'react'; import { GitAuthModel } from '@/react/portainer/gitops/types'; -import { - AuthTypeOption, - GitCredential, -} from '@/react/portainer/account/git-credentials/types'; +import { AuthTypeOption } from '@/react/portainer/account/git-credentials/types'; import { SwitchField } from '@@/form-components/SwitchField'; import { TextTip } from '@@/Tip/TextTip'; import { isBE } from '../../feature-flags/feature-flags.service'; -import { CredentialSelector } from './CredentialSelector'; import { CredentialsSection } from './CredentialsSection'; interface Props { @@ -57,43 +53,16 @@ export function AuthFieldset({ )} - {isBE && ( - - )} - - {!value.RepositoryGitCredentialID && ( - - )} + )} ); - function handleChangeGitCredential(gitCredential?: GitCredential | null) { - handleChange( - gitCredential - ? { - RepositoryGitCredentialID: gitCredential.id, - RepositoryUsername: gitCredential?.username, - RepositoryPassword: '', - SaveCredential: false, - NewCredentialName: '', - } - : { - RepositoryGitCredentialID: 0, - RepositoryUsername: '', - RepositoryPassword: '', - } - ); - } - function handleChange(partialValue: Partial) { onChange(partialValue); setValue((value) => ({ ...value, ...partialValue })); @@ -101,51 +70,31 @@ export function AuthFieldset({ } export function gitAuthValidation( - gitCredentials: Array, isAuthEdit: boolean, isCreatedFromCustomTemplate: boolean ): SchemaOf { return object({ RepositoryAuthentication: boolean().default(false), - RepositoryGitCredentialID: number().default(0), RepositoryUsername: string() - .when(['RepositoryAuthentication', 'RepositoryGitCredentialID'], { - is: (auth: boolean, id: number) => auth && !id, + .when(['RepositoryAuthentication'], { + is: (auth: boolean) => auth, then: string().required('Username is required'), }) .default(''), RepositoryPassword: string() - .when(['RepositoryAuthentication', 'RepositoryGitCredentialID'], { - is: (auth: boolean, id: number) => - auth && !id && !isAuthEdit && !isCreatedFromCustomTemplate, + .when(['RepositoryAuthentication'], { + is: (auth: boolean) => + auth && !isAuthEdit && !isCreatedFromCustomTemplate, then: string().required('Personal Access Token is required'), }) .default(''), RepositoryAuthorizationType: mixed() .oneOf(Object.values(AuthTypeOption)) - .when(['RepositoryAuthentication', 'RepositoryGitCredentialID'], { - is: (auth: boolean, id: number) => - isBE && auth && !id && !isAuthEdit && !isCreatedFromCustomTemplate, + .when(['RepositoryAuthentication'], { + is: (auth: boolean) => + isBE && auth && !isAuthEdit && !isCreatedFromCustomTemplate, then: mixed().required('Authorization type is required'), }) .default(AuthTypeOption.Basic), - SaveCredential: boolean().default(false), - NewCredentialName: string() - .default('') - .when(['RepositoryAuthentication', 'SaveCredential'], { - is: (RepositoryAuthentication: boolean, SaveCredential: boolean) => - RepositoryAuthentication && SaveCredential && !isAuthEdit, - then: string() - .required('Name is required') - .test( - 'is-unique', - 'This name is already been used, please try another one', - (name) => !!name && !gitCredentials.find((x) => x.name === name) - ) - .matches( - /^[-_a-z0-9]+$/, - "This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123')." - ), - }), }); } diff --git a/app/react/portainer/gitops/AuthFieldset/CredentialSelector.tsx b/app/react/portainer/gitops/AuthFieldset/CredentialSelector.tsx deleted file mode 100644 index 01a4f49de9..0000000000 --- a/app/react/portainer/gitops/AuthFieldset/CredentialSelector.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { GitCredential } from '@/react/portainer/account/git-credentials/types'; -import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { useUser } from '@/react/hooks/useUser'; - -import { FormControl } from '@@/form-components/FormControl'; -import { Select } from '@@/form-components/ReactSelect'; - -export function CredentialSelector({ - value, - onChange, - error, -}: { - value?: number; - onChange(gitCredential?: GitCredential | null): void; - error?: string; -}) { - const { user } = useUser(); - - const gitCredentialsQuery = useGitCredentials(user.Id); - - const gitCredentials = gitCredentialsQuery.data ?? []; - - return ( -
-
- - setUsername(e.target.value)} data-cy="component-gitUsernameInput" - readOnly={!!value.RepositoryGitCredentialID} />
@@ -95,14 +90,10 @@ export function CredentialsSection({ placeholder="*******" onChange={(e) => setPassword(e.target.value)} data-cy="component-gitPasswordInput" - readOnly={!!value.RepositoryGitCredentialID} />
- {isBE && value.RepositoryPassword && ( - - )} ); } diff --git a/app/react/portainer/gitops/AuthFieldset/NewCredentialForm.tsx b/app/react/portainer/gitops/AuthFieldset/NewCredentialForm.tsx deleted file mode 100644 index 2e0bc505fe..0000000000 --- a/app/react/portainer/gitops/AuthFieldset/NewCredentialForm.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { FormikErrors } from 'formik'; - -import { Checkbox } from '@@/form-components/Checkbox'; -import { FormControl } from '@@/form-components/FormControl'; -import { Input } from '@@/form-components/Input'; -import { TextTip } from '@@/Tip/TextTip'; - -import { GitAuthModel } from '../types'; - -export function NewCredentialForm({ - value, - onChange, - errors, -}: { - value: GitAuthModel; - onChange: (value: Partial) => void; - errors?: FormikErrors; -}) { - return ( -
-
- -
- onChange({ SaveCredential: e.target.checked })} - /> - onChange({ NewCredentialName: e.target.value })} - disabled={!value.SaveCredential} - /> - {errors?.NewCredentialName && ( -
- {errors.NewCredentialName} -
- )} - - {value.SaveCredential && ( - - This git credential can be managed through your account page - - )} -
-
-
-
- ); -} diff --git a/app/react/portainer/gitops/AuthFieldset/utils.ts b/app/react/portainer/gitops/AuthFieldset/utils.ts index ba7b4299fc..3806c4053e 100644 --- a/app/react/portainer/gitops/AuthFieldset/utils.ts +++ b/app/react/portainer/gitops/AuthFieldset/utils.ts @@ -6,18 +6,13 @@ export function parseAuthResponse( if (!auth) { return { RepositoryAuthentication: false, - NewCredentialName: '', - RepositoryGitCredentialID: 0, RepositoryPassword: '', RepositoryUsername: '', - SaveCredential: false, }; } return { RepositoryAuthentication: true, - NewCredentialName: '', - RepositoryGitCredentialID: auth.GitCredentialID, RepositoryPassword: '', RepositoryUsername: auth.Username, }; @@ -26,19 +21,12 @@ export function parseAuthResponse( export function transformGitAuthenticationViewModel( auth?: GitAuthModel ): GitAuthenticationResponse | null { - if ( - !auth || - !auth.RepositoryAuthentication || - typeof auth.RepositoryGitCredentialID === 'undefined' || - (auth.RepositoryGitCredentialID === 0 && auth.RepositoryPassword === '') - ) { + if (!auth || !auth.RepositoryAuthentication) { return null; } - if (auth.RepositoryGitCredentialID !== 0) { - return { - GitCredentialID: auth.RepositoryGitCredentialID, - }; + if (!auth.RepositoryUsername && !auth.RepositoryPassword) { + return null; } return { diff --git a/app/react/portainer/gitops/ComposePathField/PathSelector.tsx b/app/react/portainer/gitops/ComposePathField/PathSelector.tsx index 06d08cc874..6790fa4e55 100644 --- a/app/react/portainer/gitops/ComposePathField/PathSelector.tsx +++ b/app/react/portainer/gitops/ComposePathField/PathSelector.tsx @@ -10,7 +10,6 @@ export type PathSelectorGitModel = Pick< | 'RepositoryAuthentication' | 'RepositoryPassword' | 'RepositoryUsername' - | 'RepositoryGitCredentialID' | 'RepositoryAuthorizationType' | 'RepositoryURL' | 'RepositoryReferenceName' diff --git a/app/react/portainer/gitops/GitForm.stories.tsx b/app/react/portainer/gitops/GitForm.stories.tsx index e4977fe7e2..4da41b6115 100644 --- a/app/react/portainer/gitops/GitForm.stories.tsx +++ b/app/react/portainer/gitops/GitForm.stories.tsx @@ -1,9 +1,7 @@ import { Meta } from '@storybook/react-webpack5'; import { Form, Formik } from 'formik'; -import { http, HttpResponse } from 'msw'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; -import { GitCredential } from '@/react/portainer/account/git-credentials/types'; import { GitForm, buildGitValidationSchema } from './GitForm'; import { DeployMethod, GitFormModel } from './types'; @@ -11,32 +9,6 @@ import { DeployMethod, GitFormModel } from './types'; export default { component: GitForm, title: 'Components/Forms/GitForm', - parameters: { - msw: { - handlers: [ - http.get<{ userId: string }, Array>( - '/api/users/:userId/gitcredentials', - ({ params }) => - HttpResponse.json([ - { - id: 1, - name: 'credential-1', - username: 'username-1', - userId: parseInt(params.userId, 10), - creationDate: 0, - }, - { - id: 2, - name: 'credential-2', - username: 'username-2', - userId: parseInt(params.userId, 10), - creationDate: 0, - }, - ]) - ), - ], - }, - }, } as Meta; const WrappedComponent = withUserProvider(GitForm); @@ -65,15 +37,13 @@ export function Primary({ AdditionalFiles: [], RepositoryReferenceName: '', ComposeFilePathInRepository: '', - NewCredentialName: '', - SaveCredential: false, TLSSkipVerify: false, }; return ( buildGitValidationSchema([], false, 'compose')} + validationSchema={() => buildGitValidationSchema(false, 'compose')} onSubmit={() => {}} > {({ values, errors, setValues }) => ( diff --git a/app/react/portainer/gitops/GitForm.tsx b/app/react/portainer/gitops/GitForm.tsx index 84c1d74f75..be02d5ee03 100644 --- a/app/react/portainer/gitops/GitForm.tsx +++ b/app/react/portainer/gitops/GitForm.tsx @@ -12,8 +12,6 @@ import { FormSection } from '@@/form-components/FormSection'; import { validateForm } from '@@/form-components/validate-form'; import { SwitchField } from '@@/form-components/SwitchField'; -import { GitCredential } from '../account/git-credentials/types'; - import { AdditionalFileField } from './AdditionalFilesField'; import { gitAuthValidation, AuthFieldset } from './AuthFieldset'; import { AutoUpdateFieldset } from './AutoUpdateFieldset'; @@ -152,24 +150,17 @@ export function GitForm({ } export async function validateGitForm( - gitCredentials: Array, formValues: GitFormModel, isCreatedFromCustomTemplate: boolean, deployMethod: DeployMethod = 'compose' ) { return validateForm( - () => - buildGitValidationSchema( - gitCredentials, - isCreatedFromCustomTemplate, - deployMethod - ), + () => buildGitValidationSchema(isCreatedFromCustomTemplate, deployMethod), formValues ); } export function buildGitValidationSchema( - gitCredentials: Array, isCreatedFromCustomTemplate: boolean, deployMethod: DeployMethod, isEdit = false @@ -200,6 +191,6 @@ export function buildGitValidationSchema( AutoUpdate: autoUpdateValidation().nullable(), TLSSkipVerify: boolean().default(false), }).concat( - gitAuthValidation(gitCredentials, isEdit, isCreatedFromCustomTemplate) + gitAuthValidation(isEdit, isCreatedFromCustomTemplate) ) as SchemaOf; } diff --git a/app/react/portainer/gitops/RefField/types.ts b/app/react/portainer/gitops/RefField/types.ts index 0bcd99afd6..e0b35f4af8 100644 --- a/app/react/portainer/gitops/RefField/types.ts +++ b/app/react/portainer/gitops/RefField/types.ts @@ -1,6 +1,6 @@ -import { GitCredentialsModel } from '../types'; +import { GitAuthModel } from '../types'; -export interface RefFieldModel extends GitCredentialsModel { +export interface RefFieldModel extends GitAuthModel { RepositoryURL: string; TLSSkipVerify?: boolean; } diff --git a/app/react/portainer/gitops/RelativePathFieldset/utils.ts b/app/react/portainer/gitops/RelativePathFieldset/utils.ts index f397ae5f8d..998ba00580 100644 --- a/app/react/portainer/gitops/RelativePathFieldset/utils.ts +++ b/app/react/portainer/gitops/RelativePathFieldset/utils.ts @@ -22,7 +22,5 @@ export const dummyGitForm: GitFormModel = { AdditionalFiles: [], RepositoryReferenceName: '', ComposeFilePathInRepository: '', - NewCredentialName: '', - SaveCredential: false, TLSSkipVerify: false, }; diff --git a/app/react/portainer/gitops/hooks/useGitRepoValidity.ts b/app/react/portainer/gitops/hooks/useGitRepoValidity.ts index e11819a829..4feb4ece36 100644 --- a/app/react/portainer/gitops/hooks/useGitRepoValidity.ts +++ b/app/react/portainer/gitops/hooks/useGitRepoValidity.ts @@ -7,7 +7,6 @@ import { useGitRefs } from '../queries/useGitRefs'; interface Creds { username?: string; password?: string; - gitCredentialId?: number; authorizationType?: AuthTypeOption; } @@ -62,8 +61,7 @@ export function useGitRepoValidity({ } ); - const hasCreds = - !!(creds?.username && creds?.password) || !!creds?.gitCredentialId; + const hasCreds = !!(creds?.username && creds?.password); const errorMessage = getGitValidityError(query.error, hasCreds); diff --git a/app/react/portainer/gitops/queries/useGitFilePreview.ts b/app/react/portainer/gitops/queries/useGitFilePreview.ts index c697713a81..ad2b3e146d 100644 --- a/app/react/portainer/gitops/queries/useGitFilePreview.ts +++ b/app/react/portainer/gitops/queries/useGitFilePreview.ts @@ -12,7 +12,6 @@ export interface GitFilePreviewParams { username?: string; password?: string; authorizationType?: AuthTypeOption; - gitCredentialId?: number; tlsSkipVerify?: boolean; } diff --git a/app/react/portainer/gitops/queries/useGitRefs.ts b/app/react/portainer/gitops/queries/useGitRefs.ts index cd7fc92bb4..9c71c34218 100644 --- a/app/react/portainer/gitops/queries/useGitRefs.ts +++ b/app/react/portainer/gitops/queries/useGitRefs.ts @@ -12,7 +12,6 @@ interface RefsPayload { username?: string; password?: string; authorizationType?: AuthTypeOption; - gitCredentialId?: number; stackId?: number; fromEdgeStack?: boolean; createdFromCustomTemplateID?: number; diff --git a/app/react/portainer/gitops/queries/useUpdateGitStack.ts b/app/react/portainer/gitops/queries/useUpdateGitStack.ts index 2fabdbf939..f72e55944e 100644 --- a/app/react/portainer/gitops/queries/useUpdateGitStack.ts +++ b/app/react/portainer/gitops/queries/useUpdateGitStack.ts @@ -18,7 +18,6 @@ interface DeployGitPayload { Prune?: boolean; // RepullImageAndRedeploy indicates whether to force repulling images and redeploying the stack RepullImageAndRedeploy?: boolean; - RepositoryGitCredentialID?: number; StackName?: string; } diff --git a/app/react/portainer/gitops/queries/useUpdateGitStackSettings.ts b/app/react/portainer/gitops/queries/useUpdateGitStackSettings.ts index 6490d8f54b..48f6ff8e59 100644 --- a/app/react/portainer/gitops/queries/useUpdateGitStackSettings.ts +++ b/app/react/portainer/gitops/queries/useUpdateGitStackSettings.ts @@ -17,7 +17,6 @@ export interface GitStackPayload { ConfigFilePath?: string; RepositoryReferenceName?: string; RepositoryAuthentication?: boolean; - RepositoryGitCredentialID?: number; RepositoryUsername?: string; RepositoryPassword?: string; RepositoryAuthorizationType?: AuthTypeOption; diff --git a/app/react/portainer/gitops/types.ts b/app/react/portainer/gitops/types.ts index 724136e143..8a82900461 100644 --- a/app/react/portainer/gitops/types.ts +++ b/app/react/portainer/gitops/types.ts @@ -1,7 +1,4 @@ -import { - AuthTypeOption, - GitCredential, -} from '@/react/portainer/account/git-credentials/types'; +import { AuthTypeOption } from '@/react/portainer/account/git-credentials/types'; import { AutoUpdateModel, @@ -20,7 +17,6 @@ export interface GitAuthenticationResponse { Username?: string; Password?: string; AuthorizationType?: AuthTypeOption; - GitCredentialID?: number; } export interface RepoConfigResponse { @@ -32,21 +28,13 @@ export interface RepoConfigResponse { TLSSkipVerify: boolean; } -export type GitCredentialsModel = { +export type GitAuthModel = { RepositoryAuthentication?: boolean; RepositoryUsername?: string; RepositoryPassword?: string; - RepositoryGitCredentialID?: GitCredential['id']; RepositoryAuthorizationType?: AuthTypeOption; }; -export type GitNewCredentialModel = { - NewCredentialName?: string; - SaveCredential?: boolean; -}; - -export type GitAuthModel = GitCredentialsModel & GitNewCredentialModel; - export type DeployMethod = 'compose' | 'manifest' | 'helm'; export interface GitFormModel extends GitAuthModel { @@ -94,14 +82,10 @@ export function toGitFormModel( RepositoryURL: URL, ComposeFilePathInRepository: ConfigFilePath, RepositoryReferenceName: ReferenceName, - RepositoryAuthentication: !!( - Authentication && - (Authentication?.GitCredentialID || Authentication?.Username) - ), + RepositoryAuthentication: !!Authentication?.Username, RepositoryUsername: Authentication?.Username, RepositoryPassword: Authentication?.Password, RepositoryAuthorizationType: Authentication?.AuthorizationType, - RepositoryGitCredentialID: Authentication?.GitCredentialID, TLSSkipVerify, AutoUpdate: autoUpdate, }; diff --git a/app/react/portainer/gitops/utils.ts b/app/react/portainer/gitops/utils.ts index 7d7dcc893e..20b933ed5c 100644 --- a/app/react/portainer/gitops/utils.ts +++ b/app/react/portainer/gitops/utils.ts @@ -7,20 +7,13 @@ import { GitFormModel, RepoConfigResponse } from './types'; export function getAuthentication( model: Pick< GitFormModel, - | 'RepositoryAuthentication' - | 'RepositoryPassword' - | 'RepositoryUsername' - | 'RepositoryGitCredentialID' + 'RepositoryAuthentication' | 'RepositoryPassword' | 'RepositoryUsername' > ) { if (!model.RepositoryAuthentication) { return undefined; } - if (model.RepositoryGitCredentialID) { - return { gitCredentialId: model.RepositoryGitCredentialID }; - } - return { username: model.RepositoryUsername, password: model.RepositoryPassword, diff --git a/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx b/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx index 56b73c6270..63f1e3a57f 100644 --- a/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx +++ b/app/react/portainer/templates/custom-templates/CreateView/useValidation.tsx @@ -6,8 +6,6 @@ import { validation as commonFieldsValidation } from '@/react/portainer/custom-t import { Platform } from '@/react/portainer/templates/types'; import { variablesValidation } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField'; import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm'; -import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { useCurrentUser } from '@/react/hooks/useUser'; import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates'; import { edgeFieldsetValidation } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation'; import { DeployMethod } from '@/react/portainer/gitops/types'; @@ -28,8 +26,6 @@ export function useValidation({ viewType: 'kube' | 'docker' | 'edge'; deployMethod: DeployMethod; }) { - const { user } = useCurrentUser(); - const gitCredentialsQuery = useGitCredentials(user.Id); const customTemplatesQuery = useCustomTemplates({ params: { edge: undefined, @@ -60,12 +56,7 @@ export function useValidation({ }), Git: mixed().when('Method', { is: git.value, - then: () => - buildGitValidationSchema( - gitCredentialsQuery.data || [], - false, - deployMethod - ), + then: () => buildGitValidationSchema(false, deployMethod), }), Variables: variablesValidation(), EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(), @@ -74,11 +65,6 @@ export function useValidation({ templates: customTemplatesQuery.data, }) ), - [ - customTemplatesQuery.data, - gitCredentialsQuery.data, - viewType, - deployMethod, - ] + [customTemplatesQuery.data, viewType, deployMethod] ); } diff --git a/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx b/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx index 5aa012e817..e4e9901fa9 100644 --- a/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx +++ b/app/react/portainer/templates/custom-templates/EditView/useValidation.tsx @@ -6,8 +6,6 @@ import { validation as commonFieldsValidation } from '@/react/portainer/custom-t import { Platform } from '@/react/portainer/templates/types'; import { variablesValidation } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField'; import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm'; -import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service'; -import { useCurrentUser } from '@/react/hooks/useUser'; import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates'; import { edgeFieldsetValidation } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation'; import { DeployMethod } from '@/react/portainer/gitops/types'; @@ -26,8 +24,6 @@ export function useValidation({ viewType: TemplateViewType; deployMethod: DeployMethod; }) { - const { user } = useCurrentUser(); - const gitCredentialsQuery = useGitCredentials(user.Id); const customTemplatesQuery = useCustomTemplates({ params: { edge: undefined, @@ -49,13 +45,7 @@ export function useValidation({ .default(StackType.DockerCompose), FileContent: string().required('Template is required.'), - Git: isGit - ? buildGitValidationSchema( - gitCredentialsQuery.data || [], - false, - deployMethod - ) - : mixed(), + Git: isGit ? buildGitValidationSchema(false, deployMethod) : mixed(), Variables: variablesValidation(), EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(), }).concat( @@ -64,13 +54,6 @@ export function useValidation({ currentTemplateId: templateId, }) ), - [ - customTemplatesQuery.data, - gitCredentialsQuery.data, - isGit, - templateId, - viewType, - deployMethod, - ] + [customTemplatesQuery.data, isGit, templateId, viewType, deployMethod] ); } diff --git a/app/react/portainer/templates/custom-templates/queries/useCreateTemplateMutation.ts b/app/react/portainer/templates/custom-templates/queries/useCreateTemplateMutation.ts index 267bc44038..023fb61758 100644 --- a/app/react/portainer/templates/custom-templates/queries/useCreateTemplateMutation.ts +++ b/app/react/portainer/templates/custom-templates/queries/useCreateTemplateMutation.ts @@ -16,9 +16,6 @@ import { GitFormModel } from '@/react/portainer/gitops/types'; import { DefinitionFieldValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField'; import { AccessControlFormData } from '@/react/portainer/access-control/types'; import { applyResourceControl } from '@/react/portainer/access-control/access-control.service'; -import { useCurrentUser } from '@/react/hooks/useUser'; -import { UserId } from '@/portainer/users/types'; -import { saveGitCredentialsIfNeeded } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation'; import { json2formData } from '@/portainer/helpers/json'; import { Platform } from '../../types'; @@ -43,14 +40,13 @@ interface CreateTemplatePayload { } export function useCreateTemplateMutation() { - const { user } = useCurrentUser(); const queryClient = useQueryClient(); return useMutation( async ( payload: CreateTemplatePayload & { AccessControl?: AccessControlFormData } ) => { - const template = await createTemplate(user.Id, payload); + const template = await createTemplate(payload); const resourceControl = template.ResourceControl; if (resourceControl && payload.AccessControl) { @@ -66,29 +62,26 @@ export function useCreateTemplateMutation() { ); } -function createTemplate(userId: UserId, payload: CreateTemplatePayload) { +function createTemplate(payload: CreateTemplatePayload) { switch (payload.Method) { case 'editor': return createTemplateFromText(payload); case 'upload': return createTemplateFromFile(payload); case 'repository': - return createTemplateAndGitCredential(userId, payload); + return createTemplateFromGitPayload(payload); default: throw new Error('Unknown method'); } } -async function createTemplateAndGitCredential( - userId: UserId, - { Git: gitModel, ...values }: CreateTemplatePayload -) { - const resolvedAuth = await saveGitCredentialsIfNeeded(userId, gitModel); - +function createTemplateFromGitPayload({ + Git: gitModel, + ...values +}: CreateTemplatePayload) { return createTemplateFromGit({ ...values, ...gitModel, - ...resolvedAuth, ...(values.EdgeSettings ? { EdgeSettings: { @@ -218,10 +211,6 @@ interface CustomTemplateFromGitRepositoryPayload { RepositoryUsername?: string; /** Password used in basic authentication when RepositoryAuthentication is true. */ RepositoryPassword?: string; - /** GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication - * is true and RepositoryUsername/RepositoryPassword are not provided - */ - RepositoryGitCredentialID?: number; /** Path to the Stack file inside the Git repository. */ ComposeFilePathInRepository?: string; /** Definitions of variables in the stack file. */ diff --git a/app/react/portainer/templates/custom-templates/queries/useUpdateTemplateMutation.ts b/app/react/portainer/templates/custom-templates/queries/useUpdateTemplateMutation.ts index 964ac47bcc..7842520be2 100644 --- a/app/react/portainer/templates/custom-templates/queries/useUpdateTemplateMutation.ts +++ b/app/react/portainer/templates/custom-templates/queries/useUpdateTemplateMutation.ts @@ -75,11 +75,6 @@ interface CustomTemplateUpdatePayload { RepositoryUsername?: string; /** Password used in basic authentication. Required when RepositoryAuthentication is true */ RepositoryPassword?: string; - /** - * GitCredentialID used to identify the bound git credential. - * Required when RepositoryAuthentication is true and RepositoryUsername/RepositoryPassword are not provided - */ - RepositoryGitCredentialID?: number; /** Path to the Stack file inside the Git repository */ ComposeFilePathInRepository?: string; /** Content of stack file */ diff --git a/app/setup-tests/setup-handlers/users.ts b/app/setup-tests/setup-handlers/users.ts index 489230c1ce..be8c3a5e3b 100644 --- a/app/setup-tests/setup-handlers/users.ts +++ b/app/setup-tests/setup-handlers/users.ts @@ -12,5 +12,4 @@ export const userHandlers = [ '/api/users/:userId/memberships', () => HttpResponse.json([]) ), - http.get('/api/users/:userId/gitcredentials', () => HttpResponse.json([])), ];