feat(helm): reuse existing git sources in Kubernetes Helm-from-git install [BE-13046] (#2900)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chaim Lev-Ari
2026-06-15 22:01:31 +03:00
committed by GitHub
parent 491df61fbf
commit d9673e33ec
18 changed files with 348 additions and 151 deletions
@@ -1,4 +1,4 @@
package stacks
package sources
import (
"fmt"
@@ -8,9 +8,16 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
)
// validateSourceForStack checks that the given Source exists and is a git Source, and returns it.
// 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 {
Source() dataservices.SourceService
IsErrObjectNotFound(err error) bool
}
// ValidateGitSourceAccess checks that the given Source exists and is a git Source, and returns it.
// TODO(BE-12905): enforce per-user access policies once Source ownership is introduced.
func validateSourceForStack(tx dataservices.DataStoreTx, sourceID portainer.SourceID) (*portainer.Source, *httperror.HandlerError) {
func ValidateGitSourceAccess(tx gitSourceStore, sourceID portainer.SourceID) (*portainer.Source, *httperror.HandlerError) {
src, err := tx.Source().Read(sourceID)
if err != nil {
if tx.IsErrObjectNotFound(err) {
@@ -23,5 +30,9 @@ func validateSourceForStack(tx dataservices.DataStoreTx, sourceID portainer.Sour
return nil, httperror.BadRequest(fmt.Sprintf("source %d is not a git source", sourceID), nil)
}
if src.Git == nil {
return nil, httperror.BadRequest("Source has no git configuration", nil)
}
return src, nil
}
+49
View File
@@ -0,0 +1,49 @@
package sources
import (
"net/http"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateSourceForStack_ValidGitSource_ReturnsNil(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, false)
src := &portainer.Source{
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{URL: "https://github.com/org/repo"},
}
require.NoError(t, store.Source().Create(src))
_, httpErr := ValidateGitSourceAccess(store, src.ID)
assert.Nil(t, httpErr)
}
func TestValidateSourceForStack_SourceNotFound_Returns404(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, false)
_, httpErr := ValidateGitSourceAccess(store, portainer.SourceID(999))
require.NotNil(t, httpErr)
assert.Equal(t, http.StatusNotFound, httpErr.StatusCode)
}
func TestValidateSourceForStack_NonGitSource_Returns400(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, false)
src := &portainer.Source{
Type: portainer.SourceType(99), // not a git source
}
require.NoError(t, store.Source().Create(src))
_, httpErr := ValidateGitSourceAccess(store, src.ID)
require.NotNil(t, httpErr)
assert.Equal(t, http.StatusBadRequest, httpErr.StatusCode)
}
@@ -6,7 +6,9 @@ import (
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/gitops/sources"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -19,19 +21,32 @@ type fileResponse struct {
}
type repositoryFilePreviewPayload struct {
Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"`
Reference string `json:"reference" example:"refs/heads/master"`
Username string `json:"username" example:"myGitUsername"`
Password string `json:"password" example:"myGitPassword"`
// SourceID resolves URL and auth from the stored Source record.
// When set, the inline Repository/Username/Password/TLSSkipVerify fields are ignored.
SourceID portainer.SourceID `json:"sourceID" example:"1"`
Reference string `json:"reference" example:"refs/heads/master"`
// Path to file whose content will be read
TargetFile string `json:"targetFile" example:"docker-compose.yml"`
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
// URL of a Git repository to preview.
// Deprecated: use SourceID instead
Repository string `json:"repository" example:"https://github.com/openfaas/faas"`
// Username for git authentication.
// Deprecated: use SourceID instead
Username string `json:"username" example:"myGitUsername"`
// Password for git authentication.
// Deprecated: use SourceID instead
Password string `json:"password" example:"myGitPassword"`
// TLSSkipVerify skips SSL verification when cloning the Git repository.
// Deprecated: use SourceID instead
TLSSkipVerify bool `json:"tlsSkipVerify" example:"false"`
}
func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error {
if len(payload.Repository) == 0 || !validate.IsURL(payload.Repository) {
return errors.New("invalid repository URL. Must correspond to a valid URL format")
if payload.SourceID == 0 {
if len(payload.Repository) == 0 || !validate.IsURL(payload.Repository) {
return errors.New("invalid repository URL. Must correspond to a valid URL format")
}
}
if len(payload.Reference) == 0 {
@@ -56,6 +71,7 @@ func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error {
// @param body body repositoryFilePreviewPayload true "Template details"
// @success 200 {object} fileResponse "Success"
// @failure 400 "Invalid request"
// @failure 404 "Source not found"
// @failure 500 "Server error"
// @router /gitops/repo/file/preview [post]
func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
@@ -65,6 +81,25 @@ func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *ht
return httperror.BadRequest("Invalid request payload", err)
}
repoURL := payload.Repository
username := payload.Username
password := payload.Password
tlsSkipVerify := payload.TLSSkipVerify
if payload.SourceID != 0 {
src, httpErr := sources.ValidateGitSourceAccess(handler.dataStore, payload.SourceID)
if httpErr != nil {
return httpErr
}
repoURL = src.Git.URL
if src.Git.Authentication != nil {
username = src.Git.Authentication.Username
password = src.Git.Authentication.Password
}
tlsSkipVerify = src.Git.TLSSkipVerify
}
projectPath, err := handler.fileService.GetTemporaryPath()
if err != nil {
return httperror.InternalServerError("Unable to create temporary folder", err)
@@ -73,11 +108,11 @@ func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *ht
err = handler.gitService.CloneRepository(
context.TODO(),
projectPath,
payload.Repository,
repoURL,
payload.Reference,
payload.Username,
payload.Password,
payload.TLSSkipVerify,
username,
password,
tlsSkipVerify,
)
if err != nil {
if errors.Is(err, gittypes.ErrAuthenticationFailure) {
@@ -8,6 +8,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/git/update"
"github.com/portainer/portainer/api/gitops/sources"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackbuilders"
@@ -279,7 +280,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
}
if payload.SourceID != 0 {
if _, httpErr := validateSourceForStack(handler.DataStore, payload.SourceID); httpErr != nil {
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID); httpErr != nil {
return httpErr
}
}
@@ -0,0 +1,31 @@
package stacks
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func TestComposeGitPayload_ValidateWithSourceID_URLNotRequired(t *testing.T) {
t.Parallel()
payload := &composeStackFromGitRepositoryPayload{
Name: "mystack",
SourceID: portainer.SourceID(1),
// RepositoryURL intentionally omitted
}
err := payload.Validate(nil)
assert.NoError(t, err)
}
func TestComposeGitPayload_ValidateWithoutSourceID_URLRequired(t *testing.T) {
t.Parallel()
payload := &composeStackFromGitRepositoryPayload{
Name: "mystack",
// SourceID and RepositoryURL both omitted
}
err := payload.Validate(nil)
assert.Error(t, err)
}
@@ -6,6 +6,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/git/update"
"github.com/portainer/portainer/api/gitops/sources"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackbuilders"
@@ -37,25 +38,34 @@ func createStackPayloadFromK8sFileContentPayload(name, namespace, fileContent st
}
type kubernetesGitDeploymentPayload struct {
StackName string
ComposeFormat bool
Namespace string
RepositoryURL string
RepositoryReferenceName string
StackName string
ComposeFormat bool
Namespace string
// 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
// Deprecated: use SourceID instead. Reference name of a Git repository hosting the Stack file.
RepositoryReferenceName string
// Deprecated: use SourceID instead. Use basic authentication to clone the Git repository.
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
ManifestFile string
AdditionalFiles []string
AutoUpdate *portainer.AutoUpdateSettings
// TLSSkipVerify skips SSL verification when cloning the Git repository
// Deprecated: use SourceID instead. Username used in basic authentication.
RepositoryUsername string
// Deprecated: use SourceID instead. Password used in basic authentication.
RepositoryPassword string
ManifestFile string
AdditionalFiles []string
AutoUpdate *portainer.AutoUpdateSettings
// Deprecated: use SourceID instead. TLSSkipVerify skips SSL verification when cloning the Git repository.
TLSSkipVerify bool `example:"false"`
}
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, repoSkipSSLVerify bool) stackbuilders.StackPayload {
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, repoSkipSSLVerify bool, sourceID portainer.SourceID) stackbuilders.StackPayload {
return stackbuilders.StackPayload{
StackName: name,
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
SourceID: sourceID,
URL: repoUrl,
ReferenceName: repoReference,
Authentication: repoAuthentication,
@@ -94,12 +104,13 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
}
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
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.RepositoryPassword) == 0 {
return errors.New("Invalid repository credentials. 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.RepositoryPassword) == 0 {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
}
if len(payload.ManifestFile) == 0 {
@@ -218,6 +229,12 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
}
}
if payload.SourceID != 0 {
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID); httpErr != nil {
return httpErr
}
}
stackPayload := createStackPayloadFromK8sGitPayload(payload.StackName,
payload.RepositoryURL,
payload.RepositoryReferenceName,
@@ -230,6 +247,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
payload.AdditionalFiles,
payload.AutoUpdate,
payload.TLSSkipVerify,
payload.SourceID,
)
k8sStackBuilder := stackbuilders.CreateKubernetesStackGitBuilder(handler.DataStore,
@@ -0,0 +1,67 @@
package stacks
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/require"
)
func TestKubernetesGitDeploymentPayloadValidate_WithSourceID_URLNotRequired(t *testing.T) {
t.Parallel()
p := kubernetesGitDeploymentPayload{
SourceID: portainer.SourceID(1),
ManifestFile: "manifest.yaml",
}
err := p.Validate(nil)
require.NoError(t, err)
}
func TestKubernetesGitDeploymentPayloadValidate_WithSourceID_AuthNotRequired(t *testing.T) {
t.Parallel()
p := kubernetesGitDeploymentPayload{
SourceID: portainer.SourceID(1),
RepositoryAuthentication: true,
// Password intentionally omitted — should not fail when SourceID is set
ManifestFile: "manifest.yaml",
}
err := p.Validate(nil)
require.NoError(t, err)
}
func TestKubernetesGitDeploymentPayloadValidate_WithoutSourceID_URLRequired(t *testing.T) {
t.Parallel()
p := kubernetesGitDeploymentPayload{
ManifestFile: "manifest.yaml",
// SourceID and RepositoryURL both omitted
}
err := p.Validate(nil)
require.Error(t, err)
}
func TestCreateStackPayloadFromK8sGitPayload_WithSourceID(t *testing.T) {
t.Parallel()
p := createStackPayloadFromK8sGitPayload(
"k8s-stack",
"",
"",
"",
"",
false,
false,
"default",
"manifest.yaml",
nil,
nil,
false,
portainer.SourceID(7),
)
require.Equal(t, portainer.SourceID(7), p.SourceID)
require.Equal(t, "manifest.yaml", p.ManifestFile)
}
@@ -6,6 +6,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/git/update"
"github.com/portainer/portainer/api/gitops/sources"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/stackbuilders"
"github.com/portainer/portainer/api/stacks/stackutils"
@@ -218,7 +219,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
}
if payload.SourceID != 0 {
if _, httpErr := validateSourceForStack(handler.DataStore, payload.SourceID); httpErr != nil {
if _, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID); httpErr != nil {
return httpErr
}
}
@@ -0,0 +1,31 @@
package stacks
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func TestSwarmGitPayload_ValidateWithSourceID_URLNotRequired(t *testing.T) {
t.Parallel()
payload := &swarmStackFromGitRepositoryPayload{
Name: "myswarm",
SwarmID: "swarm-abc",
SourceID: portainer.SourceID(1),
}
err := payload.Validate(nil)
assert.NoError(t, err)
}
func TestSwarmGitPayload_ValidateWithoutSourceID_URLRequired(t *testing.T) {
t.Parallel()
payload := &swarmStackFromGitRepositoryPayload{
Name: "myswarm",
SwarmID: "swarm-abc",
}
err := payload.Validate(nil)
assert.Error(t, err)
}
-105
View File
@@ -1,105 +0,0 @@
package stacks
import (
"net/http"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateSourceForStack_ValidGitSource_ReturnsNil(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, false)
src := &portainer.Source{
Type: portainer.SourceTypeGit,
Git: &gittypes.RepoConfig{URL: "https://github.com/org/repo"},
}
require.NoError(t, store.Source().Create(src))
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
_, httpErr := validateSourceForStack(store, src.ID)
assert.Nil(t, httpErr)
}
func TestValidateSourceForStack_SourceNotFound_Returns404(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, false)
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
_, httpErr := validateSourceForStack(store, portainer.SourceID(999))
require.NotNil(t, httpErr)
assert.Equal(t, http.StatusNotFound, httpErr.StatusCode)
}
func TestValidateSourceForStack_NonGitSource_Returns400(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, false)
src := &portainer.Source{
Type: portainer.SourceType(99), // not a git source
}
require.NoError(t, store.Source().Create(src))
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
_, httpErr := validateSourceForStack(store, src.ID)
require.NotNil(t, httpErr)
assert.Equal(t, http.StatusBadRequest, httpErr.StatusCode)
}
func TestComposeGitPayload_ValidateWithSourceID_URLNotRequired(t *testing.T) {
t.Parallel()
payload := &composeStackFromGitRepositoryPayload{
Name: "mystack",
SourceID: portainer.SourceID(1),
// RepositoryURL intentionally omitted
}
err := payload.Validate(nil)
assert.NoError(t, err)
}
func TestComposeGitPayload_ValidateWithoutSourceID_URLRequired(t *testing.T) {
t.Parallel()
payload := &composeStackFromGitRepositoryPayload{
Name: "mystack",
// SourceID and RepositoryURL both omitted
}
err := payload.Validate(nil)
assert.Error(t, err)
}
func TestSwarmGitPayload_ValidateWithSourceID_URLNotRequired(t *testing.T) {
t.Parallel()
payload := &swarmStackFromGitRepositoryPayload{
Name: "myswarm",
SwarmID: "swarm-abc",
SourceID: portainer.SourceID(1),
}
err := payload.Validate(nil)
assert.NoError(t, err)
}
func TestSwarmGitPayload_ValidateWithoutSourceID_URLRequired(t *testing.T) {
t.Parallel()
payload := &swarmStackFromGitRepositoryPayload{
Name: "myswarm",
SwarmID: "swarm-abc",
}
err := payload.Validate(nil)
assert.Error(t, err)
}
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/portainer/portainer/api/dataservices"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/git/update"
"github.com/portainer/portainer/api/gitops/sources"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/deployments"
@@ -193,7 +194,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
}
if payload.SourceID != 0 {
src, httpErr := validateSourceForStack(handler.DataStore, payload.SourceID)
src, httpErr := sources.ValidateGitSourceAccess(handler.DataStore, payload.SourceID)
if httpErr != nil {
return httpErr
}
@@ -12,6 +12,9 @@ export type KubernetesGitRepositoryPayload = {
composeFormat: boolean;
namespace: string;
/** When set, URL and auth are resolved from the stored Source record */
sourceId?: number;
/** URL of a Git repository hosting the Stack file */
repositoryUrl: string;
/** Reference name of a Git repository hosting the Stack file */
@@ -293,6 +293,7 @@ function createKubernetesStack({ method, payload }: KubernetesCreatePayload) {
return createKubernetesStackFromGit({
stackName: payload.name,
sourceId: payload.git.SourceId,
repositoryUrl: payload.git.RepositoryURL,
repositoryReferenceName: payload.git.RepositoryReferenceName,
manifestFile: payload.git.ComposeFilePathInRepository,
@@ -6,6 +6,7 @@ describe('Git validation', () => {
const schema = getGitValidationSchema();
const validData: GitFormValues = {
SourceId: 1,
RepositoryURL: 'https://github.com/user/repo',
RepositoryReferenceName: 'refs/heads/main',
ComposeFilePathInRepository: 'docker-compose.yml',
@@ -18,7 +19,6 @@ describe('Git validation', () => {
RepositoryAuthorizationType: undefined,
SupportRelativePath: false,
FilesystemPath: '',
SourceId: 1,
};
await expect(schema.validate(validData)).resolves.toBeDefined();
@@ -4458,14 +4458,34 @@ export type StacksKubernetesGitDeploymentPayload = {
ComposeFormat?: boolean;
ManifestFile?: string;
Namespace?: string;
/**
* Deprecated: use SourceID instead. Use basic authentication to clone the Git repository.
*/
RepositoryAuthentication?: boolean;
/**
* Deprecated: use SourceID instead. Password used in basic authentication.
*/
RepositoryPassword?: string;
/**
* Deprecated: use SourceID instead. Reference name of a Git repository hosting the Stack file.
*/
RepositoryReferenceName?: string;
/**
* Deprecated: use SourceID instead. URL of a Git repository hosting the Stack file.
*/
RepositoryURL?: string;
/**
* Deprecated: use SourceID instead. Username used in basic authentication.
*/
RepositoryUsername?: string;
/**
* SourceID references an existing Source for git credentials/URL.
* When set, the inline URL and authentication fields are ignored.
*/
SourceID?: number;
StackName?: string;
/**
* TLSSkipVerify skips SSL verification when cloning the Git repository
* Deprecated: use SourceID instead. TLSSkipVerify skips SSL verification when cloning the Git repository.
*/
TLSSkipVerify?: boolean;
};
@@ -7449,16 +7469,34 @@ export type HelmInstallChartPayload = {
export type GitopsRepositoryFilePreviewPayload = {
/**
* TLSSkipVerify skips SSL verification when cloning the Git repository
* Password for git authentication.
* Deprecated: use SourceID instead
*/
TLSSkipVerify?: boolean;
password?: string;
reference?: string;
repository: string;
/**
* URL of a Git repository to preview.
* Deprecated: use SourceID instead
*/
repository?: string;
/**
* SourceID resolves URL and auth from the stored Source record.
* When set, the inline Repository/Username/Password/TLSSkipVerify fields are ignored.
*/
sourceID?: number;
/**
* Path to file whose content will be read
*/
targetFile?: string;
/**
* TLSSkipVerify skips SSL verification when cloning the Git repository.
* Deprecated: use SourceID instead
*/
tlsSkipVerify?: boolean;
/**
* Username for git authentication.
* Deprecated: use SourceID instead
*/
username?: string;
};
@@ -11217,6 +11255,10 @@ export type GitOperationRepoFilePreviewErrors = {
* Invalid request
*/
400: unknown;
/**
* Source not found
*/
404: unknown;
/**
* Server error
*/
@@ -1172,6 +1172,7 @@ export const zStacksKubernetesGitDeploymentPayload = z.object({
RepositoryReferenceName: z.string().optional(),
RepositoryURL: z.string().optional(),
RepositoryUsername: z.string().optional(),
SourceID: z.int().optional(),
StackName: z.string().optional(),
TLSSkipVerify: z.boolean().optional(),
});
@@ -2711,11 +2712,12 @@ export const zHelmInstallChartPayload = z.object({
});
export const zGitopsRepositoryFilePreviewPayload = z.object({
TLSSkipVerify: z.boolean().optional(),
password: z.string().optional(),
reference: z.string().optional(),
repository: z.string(),
repository: z.string().optional(),
sourceID: z.int().optional(),
targetFile: z.string().optional(),
tlsSkipVerify: z.boolean().optional(),
username: z.string().optional(),
});
@@ -18,6 +18,8 @@ interface Params {
createdFromCustomTemplateId?: number;
fromEdgeStack?: boolean;
stackId?: number;
/** When set, the refs check will use credentials from the stored Source record */
sourceId?: number;
enabled?: boolean;
onSettled?(isValid?: boolean): void;
// run after onSettled, useful for clearing local flags like force
@@ -32,6 +34,7 @@ export function useGitRepoValidity({
fromEdgeStack,
createdFromCustomTemplateId,
stackId,
sourceId,
enabled,
onSettled,
onAfterSettle,
@@ -45,9 +48,10 @@ export function useGitRepoValidity({
stackId,
force,
fromEdgeStack,
sourceId,
},
{
enabled: !!url && enabled,
enabled: (!!url || !!sourceId) && enabled,
select: () => true,
suppressError: true,
onSettled(isValid) {
@@ -61,7 +65,7 @@ export function useGitRepoValidity({
}
);
const hasCreds = !!(creds?.username && creds?.password);
const hasCreds = !!(creds?.username && creds?.password) || !!sourceId;
const errorMessage = getGitValidityError(query.error, hasCreds);
@@ -13,6 +13,8 @@ export interface GitFilePreviewParams {
password?: string;
authorizationType?: AuthTypeOption;
tlsSkipVerify?: boolean;
/** When set, resolves URL and auth from the stored Source record */
sourceId?: number;
}
async function getFilePreview(params: GitFilePreviewParams): Promise<string> {
@@ -35,7 +37,10 @@ export function useGitFilePreview<TData = string>(
return useQuery({
queryKey: ['gitops', 'file-preview', omitPassword(params)],
queryFn: () => getFilePreview(params),
enabled: enabled && !!params.repository && !!params.targetFile,
enabled:
enabled &&
(!!params.repository || !!params.sourceId) &&
!!params.targetFile,
select,
retry: false,
});