feat(custom-templates): reuse existing git sources in create/update [BE-13053] (#2925)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chaim Lev-Ari
2026-06-16 21:45:35 +03:00
committed by GitHub
parent ee8e73d7f9
commit 4d539a691d
15 changed files with 330 additions and 106 deletions
@@ -11,6 +11,7 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/gitops/sources"
"github.com/portainer/portainer/api/gitops/workflows"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
@@ -202,21 +203,24 @@ type customTemplateFromGitRepositoryPayload struct {
// * 3 - kubernetes
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
// URL of a Git repository hosting the Stack file
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
// SourceID references an existing Source for git credentials/URL.
// When set, the inline URL and authentication fields are ignored.
SourceID portainer.SourceID `example:"1" validate:"required"`
// Deprecated: use SourceID instead. URL of a Git repository hosting the Stack file.
RepositoryURL string `example:"https://github.com/openfaas/faas"`
// Reference name of a Git repository hosting the Stack file
RepositoryReferenceName string `example:"refs/heads/master"`
// Use basic authentication to clone the Git repository
// Deprecated: use SourceID instead. Use basic authentication to clone the Git repository.
RepositoryAuthentication bool `example:"true"`
// Username used in basic authentication. Required when RepositoryAuthentication is true.
// Deprecated: use SourceID instead. Username used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryUsername string `example:"myGitUsername"`
// Password used in basic authentication. Required when RepositoryAuthentication is true.
// Deprecated: use SourceID instead. Password used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryPassword string `example:"myGitPassword"`
// Path to the Stack file inside the Git repository
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
// TLSSkipVerify skips SSL verification when cloning the Git repository
// Deprecated: use SourceID instead. TLSSkipVerify skips SSL verification when cloning the Git repository.
TLSSkipVerify bool `example:"false"`
// IsComposeFormat indicates if the Kubernetes template is created from a Docker Compose file
IsComposeFormat bool `example:"false"`
@@ -231,11 +235,13 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
if len(payload.Description) == 0 {
return errors.New("Invalid custom template description")
}
if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
if payload.SourceID == 0 {
if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
}
if len(payload.ComposeFilePathInRepository) == 0 {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
@@ -295,41 +301,45 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
projectPath := getProjectPath()
customTemplate.ProjectPath = projectPath
gitConfig := &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ComposeFilePathInRepository,
TLSSkipVerify: payload.TLSSkipVerify,
}
if payload.RepositoryAuthentication {
gitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
}
commitHash, err := stackutils.DownloadGitRepository(context.TODO(), *gitConfig, handler.GitService, getProjectPath)
if err != nil {
return nil, err
}
src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{
Name: gittypes.RepoName(gitConfig.URL),
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{
URL: gitConfig.URL,
Authentication: gitConfig.Authentication,
TLSSkipVerify: gitConfig.TLSSkipVerify,
},
gitConfig, httpErr := sources.ResolveRepoConfig(handler.DataStore, sources.RepoConfigInput{
SourceID: payload.SourceID,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ComposeFilePathInRepository,
RepositoryURL: payload.RepositoryURL,
TLSSkipVerify: payload.TLSSkipVerify,
RepositoryAuthentication: payload.RepositoryAuthentication,
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
})
if httpErr != nil {
return nil, httpErr
}
commitHash, err := stackutils.DownloadGitRepository(context.TODO(), gitConfig, handler.GitService, getProjectPath)
if err != nil {
return nil, err
}
sourceID := payload.SourceID
if sourceID == 0 {
src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{
Name: gittypes.RepoName(gitConfig.URL),
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{
URL: gitConfig.URL,
Authentication: gitConfig.Authentication,
TLSSkipVerify: gitConfig.TLSSkipVerify,
},
})
if err != nil {
return nil, err
}
sourceID = src.ID
}
customTemplate.Artifact = &portainer.Artifact{
Files: []portainer.ArtifactFile{{
SourceID: src.ID,
SourceID: sourceID,
Path: gitConfig.ConfigFilePath,
Ref: gitConfig.ReferenceName,
Hash: commitHash,
@@ -12,6 +12,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
"github.com/gorilla/mux"
@@ -1035,3 +1036,69 @@ func TestCustomTemplateCreate_FromRepository_Validation_InvalidType(t *testing.T
require.NotNil(t, herr)
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
}
func TestCustomTemplateCreate_FromRepository_WithSourceID_Success(t *testing.T) {
t.Parallel()
handler, ds, _ := newTestHandler(t)
handler.GitService = &gitServiceCreatingFile{}
var srcID portainer.SourceID
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
src := &portainer.Source{
Name: "example/repo",
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{
URL: "https://github.com/example/repo",
},
}
err := tx.Source().Create(src)
require.NoError(t, err)
srcID = src.ID
return nil
}))
payload := customTemplateFromGitRepositoryPayload{
Title: "Source Template",
Description: "Created from source ID",
SourceID: srcID,
Type: portainer.DockerComposeStack,
Platform: portainer.CustomTemplatePlatformLinux,
}
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
rr := httptest.NewRecorder()
herr := handler.customTemplateCreate(rr, r)
require.Nil(t, herr)
require.Equal(t, http.StatusOK, rr.Code)
var tmpl portainer.CustomTemplate
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
require.NotNil(t, tmpl.Artifact)
require.Len(t, tmpl.Artifact.Files, 1)
require.Equal(t, srcID, tmpl.Artifact.Files[0].SourceID)
require.Equal(t, "deadbeef123", tmpl.Artifact.Files[0].Hash)
}
func TestCustomTemplateCreate_FromRepository_WithSourceID_NonExistentSource(t *testing.T) {
t.Parallel()
handler, _, _ := newTestHandler(t)
handler.GitService = &gitServiceCreatingFile{}
payload := customTemplateFromGitRepositoryPayload{
Title: "Source Template",
Description: "Created from non-existent source ID",
SourceID: 999,
Type: portainer.DockerComposeStack,
Platform: portainer.CustomTemplatePlatformLinux,
}
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
rr := httptest.NewRecorder()
herr := handler.customTemplateCreate(rr, r)
require.NotNil(t, herr)
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
}
@@ -12,6 +12,7 @@ import (
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/git"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/gitops/sources"
"github.com/portainer/portainer/api/gitops/workflows"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
@@ -36,16 +37,18 @@ type customTemplateUpdatePayload struct {
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
// URL of a Git repository hosting the Stack file
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
// SourceID references an existing Source for git credentials/URL.
// When set, the inline URL and authentication fields are ignored.
SourceID portainer.SourceID `example:"1"`
// Deprecated: use SourceID instead. URL of a Git repository hosting the Stack file.
RepositoryURL string `example:"https://github.com/openfaas/faas"`
// Reference name of a Git repository hosting the Stack file
RepositoryReferenceName string `example:"refs/heads/master"`
// Use authentication to clone the Git repository
// Deprecated: use SourceID instead. Use authentication to clone the Git repository.
RepositoryAuthentication bool `example:"true"`
// Username used in basic authentication. Required when RepositoryAuthentication is true
// Deprecated: use SourceID instead. Username used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryUsername string `example:"myGitUsername"`
// Password used in basic authentication or token used in token authentication.
// Required when RepositoryAuthentication is true
// Deprecated: use SourceID instead. Password used in basic authentication or token used in token authentication. Required when RepositoryAuthentication is true.
RepositoryPassword string `example:"myGitPassword"`
// Path to the Stack file inside the Git repository
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
@@ -53,7 +56,7 @@ type customTemplateUpdatePayload struct {
FileContent string `validate:"required"`
// Definitions of variables in the stack file
Variables []portainer.CustomTemplateVariableDefinition
// TLSSkipVerify skips SSL verification when cloning the Git repository
// Deprecated: use SourceID instead. TLSSkipVerify skips SSL verification when cloning the Git repository.
TLSSkipVerify bool `example:"false"`
// IsComposeFormat indicates if the Kubernetes template is created from a Docker Compose file
IsComposeFormat bool `example:"false"`
@@ -66,10 +69,6 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid custom template title")
}
if len(payload.FileContent) == 0 && len(payload.RepositoryURL) == 0 {
return errors.New("Either file content or git repository url need to be provided")
}
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
return errors.New("Invalid custom template platform")
}
@@ -86,8 +85,19 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid note. <img> tag is not supported")
}
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
if len(payload.FileContent) == 0 && payload.SourceID == 0 {
if len(payload.RepositoryURL) == 0 {
return errors.New("Either file content, git repository url, or source ID need to be provided")
}
if !validate.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
}
if len(payload.ComposeFilePathInRepository) == 0 {
@@ -172,35 +182,33 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
customTemplate.IsComposeFormat = payload.IsComposeFormat
customTemplate.EdgeTemplate = payload.EdgeTemplate
if payload.RepositoryURL != "" {
if !validate.IsURL(payload.RepositoryURL) {
return httperror.BadRequest("Invalid repository URL. Must correspond to a valid URL format", err)
if payload.SourceID != 0 || payload.RepositoryURL != "" {
gitConfig, httpErr := sources.ResolveRepoConfig(handler.DataStore, sources.RepoConfigInput{
SourceID: payload.SourceID,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ComposeFilePathInRepository,
RepositoryURL: payload.RepositoryURL,
TLSSkipVerify: payload.TLSSkipVerify,
RepositoryAuthentication: payload.RepositoryAuthentication,
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
})
if httpErr != nil {
return httpErr
}
gitConfig := &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ComposeFilePathInRepository,
TLSSkipVerify: payload.TLSSkipVerify,
}
repositoryUsername := ""
repositoryPassword := ""
if payload.RepositoryAuthentication {
repositoryUsername = payload.RepositoryUsername
repositoryPassword = payload.RepositoryPassword
gitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
var username, password string
if gitConfig.Authentication != nil {
username = gitConfig.Authentication.Username
password = gitConfig.Authentication.Password
}
cleanBackup, err := git.CloneWithBackup(context.TODO(), handler.GitService, handler.FileService, git.CloneOptions{
ProjectPath: customTemplate.ProjectPath,
URL: gitConfig.URL,
ReferenceName: gitConfig.ReferenceName,
Username: repositoryUsername,
Password: repositoryPassword,
Username: username,
Password: password,
TLSSkipVerify: gitConfig.TLSSkipVerify,
})
if err != nil {
@@ -213,30 +221,34 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
context.TODO(),
gitConfig.URL,
gitConfig.ReferenceName,
repositoryUsername,
repositoryPassword,
username,
password,
gitConfig.TLSSkipVerify,
)
if err != nil {
return httperror.InternalServerError("Unable get latest commit id", fmt.Errorf("failed to fetch latest commit id of the template %v: %w", customTemplate.ID, err))
}
src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{
Name: gittypes.RepoName(gitConfig.URL),
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{
URL: gitConfig.URL,
Authentication: gitConfig.Authentication,
TLSSkipVerify: gitConfig.TLSSkipVerify,
},
})
if err != nil {
return httperror.InternalServerError("Unable to find or create git source", err)
sourceID := payload.SourceID
if sourceID == 0 {
src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{
Name: gittypes.RepoName(gitConfig.URL),
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{
URL: gitConfig.URL,
Authentication: gitConfig.Authentication,
TLSSkipVerify: gitConfig.TLSSkipVerify,
},
})
if err != nil {
return httperror.InternalServerError("Unable to find or create git source", err)
}
sourceID = src.ID
}
customTemplate.Artifact = &portainer.Artifact{
Files: []portainer.ArtifactFile{{
SourceID: src.ID,
SourceID: sourceID,
Path: gitConfig.ConfigFilePath,
Ref: gitConfig.ReferenceName,
Hash: commitHash,
@@ -9,6 +9,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
"github.com/gorilla/mux"
@@ -448,6 +449,95 @@ func TestCustomTemplateUpdate_CreatorDeniedWhenAdminOnly(t *testing.T) {
require.Equal(t, http.StatusForbidden, herr.StatusCode)
}
func TestCustomTemplateUpdate_WithSourceID_Success(t *testing.T) {
t.Parallel()
handler, ds, _ := newTestHandler(t)
handler.GitService = &gitServiceCreatingFile{}
projectDir := t.TempDir()
var srcID portainer.SourceID
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{
ID: 1,
Title: "Source Template",
EntryPoint: filesystem.ComposeFileDefaultName,
Type: portainer.DockerComposeStack,
Platform: portainer.CustomTemplatePlatformLinux,
CreatedByUserID: 1,
ProjectPath: projectDir,
}))
src := &portainer.Source{
Name: "example/repo",
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{
URL: "https://github.com/example/repo",
},
}
err := tx.Source().Create(src)
require.NoError(t, err)
srcID = src.ID
return nil
}))
payload := customTemplateUpdatePayload{
Title: "Source Template",
Description: "Updated via source ID",
SourceID: srcID,
Type: portainer.DockerComposeStack,
Platform: portainer.CustomTemplatePlatformLinux,
}
r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
rr := httptest.NewRecorder()
herr := handler.customTemplateUpdate(rr, r)
require.Nil(t, herr)
require.Equal(t, http.StatusOK, rr.Code)
var tmpl portainer.CustomTemplate
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
require.NotNil(t, tmpl.Artifact)
require.Len(t, tmpl.Artifact.Files, 1)
require.Equal(t, srcID, tmpl.Artifact.Files[0].SourceID)
require.Equal(t, "deadbeef123", tmpl.Artifact.Files[0].Hash)
}
func TestCustomTemplateUpdate_WithSourceID_NonExistentSource(t *testing.T) {
t.Parallel()
handler, ds, _ := newTestHandler(t)
handler.GitService = &gitServiceCreatingFile{}
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
return tx.CustomTemplate().Create(&portainer.CustomTemplate{
ID: 1,
Title: "Source Template",
EntryPoint: filesystem.ComposeFileDefaultName,
Type: portainer.DockerComposeStack,
Platform: portainer.CustomTemplatePlatformLinux,
CreatedByUserID: 1,
})
}))
payload := customTemplateUpdatePayload{
Title: "Source Template",
Description: "Updated via non-existent source ID",
SourceID: 999,
Type: portainer.DockerComposeStack,
Platform: portainer.CustomTemplatePlatformLinux,
}
r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
rr := httptest.NewRecorder()
herr := handler.customTemplateUpdate(rr, r)
require.NotNil(t, herr)
require.Equal(t, http.StatusNotFound, herr.StatusCode)
}
func TestCustomTemplateUpdate_AdminCanUpdateAdminOnly(t *testing.T) {
t.Parallel()
@@ -8018,12 +8018,11 @@ export type CustomtemplatesCustomTemplateUpdatePayload = {
*/
Platform?: 1 | 2;
/**
* Use authentication to clone the Git repository
* Deprecated: use SourceID instead. Use authentication to clone the Git repository.
*/
RepositoryAuthentication?: boolean;
/**
* Password used in basic authentication or token used in token authentication.
* Required when RepositoryAuthentication is true
* Deprecated: use SourceID instead. Password used in basic authentication or token used in token authentication. Required when RepositoryAuthentication is true.
*/
RepositoryPassword?: string;
/**
@@ -8031,15 +8030,20 @@ export type CustomtemplatesCustomTemplateUpdatePayload = {
*/
RepositoryReferenceName?: string;
/**
* URL of a Git repository hosting the Stack file
* Deprecated: use SourceID instead. URL of a Git repository hosting the Stack file.
*/
RepositoryURL: string;
RepositoryURL?: string;
/**
* Username used in basic authentication. Required when RepositoryAuthentication is true
* Deprecated: use SourceID instead. Username used in basic authentication. Required when RepositoryAuthentication is true.
*/
RepositoryUsername?: string;
/**
* TLSSkipVerify skips SSL verification when cloning the Git repository
* SourceID references an existing Source for git credentials/URL.
* When set, the inline URL and authentication fields are ignored.
*/
SourceID?: number;
/**
* Deprecated: use SourceID instead. TLSSkipVerify skips SSL verification when cloning the Git repository.
*/
TLSSkipVerify?: boolean;
/**
@@ -8088,11 +8092,11 @@ export type CustomtemplatesCustomTemplateFromGitRepositoryPayload = {
*/
Platform?: 1 | 2;
/**
* Use basic authentication to clone the Git repository
* Deprecated: use SourceID instead. Use basic authentication to clone the Git repository.
*/
RepositoryAuthentication?: boolean;
/**
* Password used in basic authentication. Required when RepositoryAuthentication is true.
* Deprecated: use SourceID instead. Password used in basic authentication. Required when RepositoryAuthentication is true.
*/
RepositoryPassword?: string;
/**
@@ -8100,15 +8104,20 @@ export type CustomtemplatesCustomTemplateFromGitRepositoryPayload = {
*/
RepositoryReferenceName?: string;
/**
* URL of a Git repository hosting the Stack file
* Deprecated: use SourceID instead. URL of a Git repository hosting the Stack file.
*/
RepositoryURL: string;
RepositoryURL?: string;
/**
* Username used in basic authentication. Required when RepositoryAuthentication is true.
* Deprecated: use SourceID instead. Username used in basic authentication. Required when RepositoryAuthentication is true.
*/
RepositoryUsername?: string;
/**
* TLSSkipVerify skips SSL verification when cloning the Git repository
* SourceID references an existing Source for git credentials/URL.
* When set, the inline URL and authentication fields are ignored.
*/
SourceID: number;
/**
* Deprecated: use SourceID instead. TLSSkipVerify skips SSL verification when cloning the Git repository.
*/
TLSSkipVerify?: boolean;
/**
@@ -3025,8 +3025,9 @@ export const zCustomtemplatesCustomTemplateUpdatePayload = z.object({
RepositoryAuthentication: z.boolean().optional(),
RepositoryPassword: z.string().optional(),
RepositoryReferenceName: z.string().optional(),
RepositoryURL: z.string(),
RepositoryURL: z.string().optional(),
RepositoryUsername: z.string().optional(),
SourceID: z.int().optional(),
TLSSkipVerify: z.boolean().optional(),
Title: z.string(),
Type: z.union([z.literal(1), z.literal(2), z.literal(3)]),
@@ -3047,8 +3048,9 @@ export const zCustomtemplatesCustomTemplateFromGitRepositoryPayload = z.object({
RepositoryAuthentication: z.boolean().optional(),
RepositoryPassword: z.string().optional(),
RepositoryReferenceName: z.string().optional(),
RepositoryURL: z.string(),
RepositoryURL: z.string().optional(),
RepositoryUsername: z.string().optional(),
SourceID: z.int(),
TLSSkipVerify: z.boolean().optional(),
Title: z.string(),
Type: z.union([z.literal(1), z.literal(2)]),
@@ -131,6 +131,7 @@ export function InnerForm({
}))
}
errors={errors.Git}
isSourceSelectionVisible
/>
)}
@@ -56,7 +56,8 @@ export function useValidation({
}),
Git: mixed().when('Method', {
is: git.value,
then: () => buildGitValidationSchema(false, deployMethod),
then: () =>
buildGitValidationSchema(false, deployMethod, false, true),
}),
Variables: variablesValidation(),
EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(),
@@ -41,6 +41,7 @@ export function EditForm({
isGit,
templateId: template.Id,
deployMethod,
isSourceSelection: isGit,
});
const fileContentQuery = useCustomTemplateFile(template.Id);
@@ -119,6 +119,7 @@ export function InnerForm({
values.Type === StackType.Kubernetes ? 'manifest' : 'compose'
}
errors={typeof errors.Git === 'object' ? errors.Git : undefined}
isSourceSelectionVisible={!!values.Git.SourceId}
/>
<div className="form-group">
<div className="col-sm-12">
@@ -33,7 +33,12 @@ export function useInitialValues({
Note: template.Note,
Logo: template.Logo,
Variables: template.Variables,
Git: template.GitConfig ? toGitFormModel(template.GitConfig) : undefined,
Git: template.GitConfig
? {
...toGitFormModel(template.GitConfig),
SourceId: template.artifact?.files?.[0]?.sourceId,
}
: undefined,
AccessControl:
!isEdge && template.ResourceControl
? parseAccessControlFormData(
@@ -18,11 +18,13 @@ export function useValidation({
templateId,
viewType,
deployMethod,
isSourceSelection,
}: {
isGit: boolean;
templateId: CustomTemplate['Id'];
viewType: TemplateViewType;
deployMethod: DeployMethod;
isSourceSelection?: boolean;
}) {
const customTemplatesQuery = useCustomTemplates({
params: {
@@ -45,7 +47,14 @@ export function useValidation({
.default(StackType.DockerCompose),
FileContent: string().required('Template is required.'),
Git: isGit ? buildGitValidationSchema(false, deployMethod) : mixed(),
Git: isGit
? buildGitValidationSchema(
false,
deployMethod,
false,
isSourceSelection ?? false
)
: mixed(),
Variables: variablesValidation(),
EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(),
}).concat(
@@ -54,6 +63,13 @@ export function useValidation({
currentTemplateId: templateId,
})
),
[customTemplatesQuery.data, isGit, templateId, viewType, deployMethod]
[
customTemplatesQuery.data,
isGit,
isSourceSelection,
templateId,
viewType,
deployMethod,
]
);
}
@@ -201,6 +201,8 @@ interface CustomTemplateFromGitRepositoryPayload {
Platform: Platform;
/** Type of created stack. Required. */
Type: StackType;
/** References an existing Source for git credentials/URL. When set, inline URL and auth are ignored. */
SourceId?: number;
/** URL of a Git repository hosting the Stack file. Required. */
RepositoryURL: string;
/** Reference name of a Git repository hosting the Stack file. */
@@ -65,6 +65,8 @@ interface CustomTemplateUpdatePayload {
* Required
*/
Type: StackType;
/** References an existing Source for git credentials/URL. When set, inline URL and auth are ignored. */
SourceId?: number;
/** URL of a Git repository hosting the Stack file */
RepositoryURL?: string;
/** Reference name of a Git repository hosting the Stack file */
@@ -96,6 +96,11 @@ export type CustomTemplate = {
EdgeTemplate: boolean;
EdgeSettings?: EdgeTemplateSettings;
/** Artifact stores the git source reference for git-backed templates. */
artifact?: {
files?: Array<{ sourceId?: number }>;
};
};
/**