mirror of
https://github.com/portainer/portainer.git
synced 2026-06-23 04:10:29 +00:00
feat(gitcredential): remove GitCredential BE-12919 (#2838)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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<GitCredential> = [];
|
||||
|
||||
Authentication: IAuthenticationService;
|
||||
|
||||
value?: GitAuthModel;
|
||||
|
||||
isAuthEdit: boolean;
|
||||
@@ -29,12 +20,8 @@ export default class GitFormAuthFieldsetController {
|
||||
onChange?: (value: GitAuthModel) => void;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async: <T>(fn: () => Promise<T>) => Promise<T>,
|
||||
Authentication: IAuthenticationService
|
||||
) {
|
||||
constructor($async: <T>(fn: () => Promise<T>) => Promise<T>) {
|
||||
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<GitAuthModel>(
|
||||
() => 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GitFormModel>;
|
||||
@@ -16,10 +11,6 @@ export default class GitFormController {
|
||||
|
||||
gitForm?: IFormController;
|
||||
|
||||
gitCredentials: Array<GitCredential> = [];
|
||||
|
||||
Authentication: IAuthenticationService;
|
||||
|
||||
value?: GitFormModel;
|
||||
|
||||
onChange?: (value: GitFormModel) => void;
|
||||
@@ -29,12 +20,8 @@ export default class GitFormController {
|
||||
deployMethod?: DeployMethod;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async: <T>(fn: () => Promise<T>) => Promise<T>,
|
||||
Authentication: IAuthenticationService
|
||||
) {
|
||||
constructor($async: <T>(fn: () => Promise<T>) => Promise<T>) {
|
||||
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');
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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<FormValues> {
|
||||
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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -102,7 +102,6 @@ export interface GitStackPayload {
|
||||
prune?: boolean;
|
||||
RepositoryReferenceName?: string;
|
||||
RepositoryAuthentication?: boolean;
|
||||
RepositoryGitCredentialID?: number;
|
||||
RepositoryUsername?: string;
|
||||
RepositoryPassword?: string;
|
||||
RepositoryAuthorizationType?: AuthTypeOption;
|
||||
|
||||
@@ -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: '',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<GitCredential> | undefined;
|
||||
}): SchemaOf<GitFormValues> {
|
||||
return buildGitValidationSchema(gitCredentials, false, 'compose').concat(
|
||||
export function getGitValidationSchema(): SchemaOf<GitFormValues> {
|
||||
return buildGitValidationSchema(false, 'compose').concat(
|
||||
object({
|
||||
SupportRelativePath: boolean().default(false),
|
||||
FilesystemPath: string()
|
||||
|
||||
@@ -27,15 +27,12 @@ export function mockFormValues(overrides: DeepPartial<FormValues>): FormValues {
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
RepositoryGitCredentialID: 0,
|
||||
TLSSkipVerify: false,
|
||||
AdditionalFiles: [],
|
||||
AutoUpdate: undefined,
|
||||
RepositoryAuthorizationType: undefined,
|
||||
SupportRelativePath: false,
|
||||
FilesystemPath: '',
|
||||
SaveCredential: false,
|
||||
NewCredentialName: '',
|
||||
},
|
||||
template: {
|
||||
selectedId: undefined,
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Stack>;
|
||||
containerNames?: Array<string>;
|
||||
gitCredentials?: Array<GitCredential>;
|
||||
}): SchemaOf<FormValues> {
|
||||
return getBaseValidationSchema({ isAdmin, environmentId, stacks }).concat(
|
||||
object({
|
||||
git: getGitValidationSchema({ gitCredentials }).when('method', {
|
||||
git: getGitValidationSchema().when('method', {
|
||||
is: 'repository',
|
||||
then: (schema) => schema.required(),
|
||||
otherwise: () => mixed(),
|
||||
|
||||
@@ -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<SchemaOf<FormValues>> {
|
||||
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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ const expectedCustomTemplatePayload = {
|
||||
repositoryReferenceName: 'refs/heads/main',
|
||||
filePathInRepository: 'docker/voting.yaml',
|
||||
repositoryAuthentication: false,
|
||||
repositoryGitCredentialId: 0,
|
||||
repositoryPassword: '',
|
||||
filesystemPath: '/test',
|
||||
supportRelativePath: true,
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,7 +46,6 @@ describe('GitForm', () => {
|
||||
Authentication: {
|
||||
Username: '',
|
||||
Password: '',
|
||||
RepositoryGitCredentialID: 0,
|
||||
},
|
||||
},
|
||||
PrePullImage: false,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<GitCredential[]>(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<GitCredential>(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<GitCredential[]>(
|
||||
buildGitUrl(credential.userId, credential.id)
|
||||
);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to delete git credential');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGitCredential(
|
||||
credential: Partial<UpdateGitCredentialPayload>,
|
||||
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`;
|
||||
}
|
||||
-124
@@ -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<GitCredentialsModel> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: () => (
|
||||
<div data-cy="credential-selector">Credential Selector</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./NewCredentialForm', () => ({
|
||||
NewCredentialForm: () => (
|
||||
<div data-cy="new-credential-form">New Credential Form</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
</TextTip>
|
||||
)}
|
||||
|
||||
{isBE && (
|
||||
<CredentialSelector
|
||||
onChange={handleChangeGitCredential}
|
||||
value={value.RepositoryGitCredentialID}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!value.RepositoryGitCredentialID && (
|
||||
<CredentialsSection
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
errors={errors}
|
||||
/>
|
||||
)}
|
||||
<CredentialsSection
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
errors={errors}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
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<GitAuthModel>) {
|
||||
onChange(partialValue);
|
||||
setValue((value) => ({ ...value, ...partialValue }));
|
||||
@@ -101,51 +70,31 @@ export function AuthFieldset({
|
||||
}
|
||||
|
||||
export function gitAuthValidation(
|
||||
gitCredentials: Array<GitCredential>,
|
||||
isAuthEdit: boolean,
|
||||
isCreatedFromCustomTemplate: boolean
|
||||
): SchemaOf<GitAuthModel> {
|
||||
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')."
|
||||
),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<FormControl
|
||||
label="Git Credentials"
|
||||
inputId="git-creds-selector"
|
||||
errors={error}
|
||||
>
|
||||
<Select
|
||||
placeholder="select git credential or fill in below"
|
||||
value={gitCredentials.find(
|
||||
(gitCredential) => gitCredential.id === value
|
||||
)}
|
||||
options={gitCredentials}
|
||||
getOptionLabel={(gitCredential) => gitCredential.name}
|
||||
getOptionValue={(gitCredential) => gitCredential.id.toString()}
|
||||
onChange={onChange}
|
||||
isClearable
|
||||
noOptionsMessage={() => 'no saved credentials'}
|
||||
inputId="git-creds-selector"
|
||||
data-cy="git-credentials-selector"
|
||||
id="git-credentials-selector"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import { AuthTypeOption } from '../../account/git-credentials/types';
|
||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||
import { GitAuthModel } from '../types';
|
||||
|
||||
import { NewCredentialForm } from './NewCredentialForm';
|
||||
|
||||
export const defaultAuthTypeOptions = [
|
||||
{
|
||||
value: AuthTypeOption.Basic,
|
||||
@@ -71,12 +69,9 @@ export function CredentialsSection({
|
||||
<Input
|
||||
value={username}
|
||||
name="repository_username"
|
||||
placeholder={
|
||||
value.RepositoryGitCredentialID ? '' : 'git username'
|
||||
}
|
||||
placeholder="git username"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
data-cy="component-gitUsernameInput"
|
||||
readOnly={!!value.RepositoryGitCredentialID}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -95,14 +90,10 @@ export function CredentialsSection({
|
||||
placeholder="*******"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
data-cy="component-gitPasswordInput"
|
||||
readOnly={!!value.RepositoryGitCredentialID}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
{isBE && value.RepositoryPassword && (
|
||||
<NewCredentialForm value={value} onChange={onChange} errors={errors} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<GitAuthModel>) => void;
|
||||
errors?: FormikErrors<GitAuthModel>;
|
||||
}) {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<FormControl label="">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="repository-save-credential"
|
||||
data-cy="gitops-save-credential-checkbox"
|
||||
label="save credential"
|
||||
checked={value.SaveCredential || false}
|
||||
className="[&+label]:mb-0"
|
||||
onChange={(e) => onChange({ SaveCredential: e.target.checked })}
|
||||
/>
|
||||
<Input
|
||||
value={value.NewCredentialName || ''}
|
||||
data-cy="gitops-new-credential-name-input"
|
||||
name="new_credential_name"
|
||||
placeholder="credential name"
|
||||
className="ml-4 w-48"
|
||||
onChange={(e) => onChange({ NewCredentialName: e.target.value })}
|
||||
disabled={!value.SaveCredential}
|
||||
/>
|
||||
{errors?.NewCredentialName && (
|
||||
<div className="small text-danger">
|
||||
{errors.NewCredentialName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{value.SaveCredential && (
|
||||
<TextTip color="blue">
|
||||
This git credential can be managed through your account page
|
||||
</TextTip>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -10,7 +10,6 @@ export type PathSelectorGitModel = Pick<
|
||||
| 'RepositoryAuthentication'
|
||||
| 'RepositoryPassword'
|
||||
| 'RepositoryUsername'
|
||||
| 'RepositoryGitCredentialID'
|
||||
| 'RepositoryAuthorizationType'
|
||||
| 'RepositoryURL'
|
||||
| 'RepositoryReferenceName'
|
||||
|
||||
@@ -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<GitCredential>>(
|
||||
'/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 (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={() => buildGitValidationSchema([], false, 'compose')}
|
||||
validationSchema={() => buildGitValidationSchema(false, 'compose')}
|
||||
onSubmit={() => {}}
|
||||
>
|
||||
{({ values, errors, setValues }) => (
|
||||
|
||||
@@ -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<GitCredential>,
|
||||
formValues: GitFormModel,
|
||||
isCreatedFromCustomTemplate: boolean,
|
||||
deployMethod: DeployMethod = 'compose'
|
||||
) {
|
||||
return validateForm<GitFormModel>(
|
||||
() =>
|
||||
buildGitValidationSchema(
|
||||
gitCredentials,
|
||||
isCreatedFromCustomTemplate,
|
||||
deployMethod
|
||||
),
|
||||
() => buildGitValidationSchema(isCreatedFromCustomTemplate, deployMethod),
|
||||
formValues
|
||||
);
|
||||
}
|
||||
|
||||
export function buildGitValidationSchema(
|
||||
gitCredentials: Array<GitCredential>,
|
||||
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<GitFormModel>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,5 @@ export const dummyGitForm: GitFormModel = {
|
||||
AdditionalFiles: [],
|
||||
RepositoryReferenceName: '',
|
||||
ComposeFilePathInRepository: '',
|
||||
NewCredentialName: '',
|
||||
SaveCredential: false,
|
||||
TLSSkipVerify: false,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface GitFilePreviewParams {
|
||||
username?: string;
|
||||
password?: string;
|
||||
authorizationType?: AuthTypeOption;
|
||||
gitCredentialId?: number;
|
||||
tlsSkipVerify?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ interface RefsPayload {
|
||||
username?: string;
|
||||
password?: string;
|
||||
authorizationType?: AuthTypeOption;
|
||||
gitCredentialId?: number;
|
||||
stackId?: number;
|
||||
fromEdgeStack?: boolean;
|
||||
createdFromCustomTemplateID?: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface GitStackPayload {
|
||||
ConfigFilePath?: string;
|
||||
RepositoryReferenceName?: string;
|
||||
RepositoryAuthentication?: boolean;
|
||||
RepositoryGitCredentialID?: number;
|
||||
RepositoryUsername?: string;
|
||||
RepositoryPassword?: string;
|
||||
RepositoryAuthorizationType?: AuthTypeOption;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
+7
-18
@@ -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. */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -12,5 +12,4 @@ export const userHandlers = [
|
||||
'/api/users/:userId/memberships',
|
||||
() => HttpResponse.json([])
|
||||
),
|
||||
http.get('/api/users/:userId/gitcredentials', () => HttpResponse.json([])),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user