Provisioning: Allow disabling of instance sync (#111270)

---------

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Stephanie Hingtgen
2025-09-18 10:40:02 -05:00
committed by GitHub
co-authored by Ryan McKinley Alex Khomenko
parent 0ab7488305
commit cb11bc15fa
14 changed files with 95 additions and 6 deletions
@@ -14,6 +14,9 @@ type RepositoryViewList struct {
// The UI should force the onboarding workflow when this is true
LegacyStorage bool `json:"legacyStorage,omitempty"`
// The valid targets (can disable instance or folder types)
AllowedTargets []SyncTargetType `json:"allowedTargets,omitempty"`
// AvailableRepositoryTypes is the list of repository types supported in this instance (e.g. git, bitbucket, github, etc)
AvailableRepositoryTypes []RepositoryType `json:"availableRepositoryTypes,omitempty"`
@@ -817,6 +817,11 @@ func (in *RepositoryView) DeepCopy() *RepositoryView {
func (in *RepositoryViewList) DeepCopyInto(out *RepositoryViewList) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.AllowedTargets != nil {
in, out := &in.AllowedTargets, &out.AllowedTargets
*out = make([]SyncTargetType, len(*in))
copy(*out, *in)
}
if in.AvailableRepositoryTypes != nil {
in, out := &in.AvailableRepositoryTypes, &out.AvailableRepositoryTypes
*out = make([]RepositoryType, len(*in))
@@ -1734,6 +1734,22 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryViewList(ref common.Referen
Format: "",
},
},
"allowedTargets": {
SchemaProps: spec.SchemaProps{
Description: "The valid targets (can disable instance or folder types)",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"folder", "instance"},
},
},
},
},
},
"availableRepositoryTypes": {
SchemaProps: spec.SchemaProps{
Description: "AvailableRepositoryTypes is the list of repository types supported in this instance (e.g. git, bitbucket, github, etc)",
@@ -12,6 +12,7 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioni
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,RepositoryList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,RepositorySpec,Workflows
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,RepositoryView,Workflows
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,RepositoryViewList,AllowedTargets
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,RepositoryViewList,AvailableRepositoryTypes
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,RepositoryViewList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ResourceList,Items
+7
View File
@@ -2235,3 +2235,10 @@ fail_tests_on_console = true
[plugins.restricted_apis_blocklist]
# Example: Block specific plugins from accessing an API
# addPanel = "untrusted-.*, experimental-.*"
#################################### Provisioning ##########################################
[provisioning]
# List of targets that can be controlled by a repository, separated by |.
# Instance means the whole grafana instance will be controlled by a repository.
# Folder limits it to a folder within the grafana instance.
allowed_targets = instance|folder
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"time"
@@ -83,6 +84,8 @@ type APIBuilder struct {
// TODO: Set this up in the standalone API server
onlyApiServer bool
allowedTargets []provisioning.SyncTargetType
features featuremgmt.FeatureToggles
usageStats usagestats.Service
@@ -128,6 +131,7 @@ func NewAPIBuilder(
extraBuilders []ExtraBuilder,
extraWorkers []jobs.Worker,
jobHistoryConfig *JobHistoryConfig,
allowedTargets []provisioning.SyncTargetType,
) *APIBuilder {
clients := resources.NewClientFactory(configProvider)
parsers := resources.NewParserFactory(clients)
@@ -149,6 +153,7 @@ func NewAPIBuilder(
access: access,
jobHistoryConfig: jobHistoryConfig,
extraWorkers: extraWorkers,
allowedTargets: allowedTargets,
}
for _, builder := range extraBuilders {
@@ -213,6 +218,11 @@ func RegisterAPIService(
return nil, nil
}
allowedTargets := []provisioning.SyncTargetType{}
for _, target := range cfg.ProvisioningAllowedTargets {
allowedTargets = append(allowedTargets, provisioning.SyncTargetType(target))
}
builder := NewAPIBuilder(
cfg.ProvisioningDisableControllers,
repoFactory,
@@ -226,6 +236,7 @@ func RegisterAPIService(
extraBuilders,
extraWorkers,
createJobHistoryConfigFromSettings(cfg),
allowedTargets,
)
apiregistration.RegisterAPI(builder)
return builder, nil
@@ -538,6 +549,10 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
list := repository.ValidateRepository(repo)
cfg := repo.Config()
if !slices.Contains(b.allowedTargets, cfg.Spec.Sync.Target) {
return fmt.Errorf("sync target %s is not supported", cfg.Spec.Sync.Target)
}
if a.GetOperation() == admission.Update {
oldRepo, err := b.asRepository(ctx, a.GetOldObject(), nil)
if err != nil {
+2 -1
View File
@@ -162,7 +162,8 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) {
}
settings := provisioning.RepositoryViewList{
Items: make([]provisioning.RepositoryView, len(all)),
Items: make([]provisioning.RepositoryView, len(all)),
AllowedTargets: b.allowedTargets,
// FIXME: this shouldn't be here in provisioning but at the dual writer or something about the storage
LegacyStorage: legacyStorage,
AvailableRepositoryTypes: b.repoFactory.Types(),
+5
View File
@@ -134,6 +134,7 @@ type Cfg struct {
PermittedProvisioningPaths []string
// Provisioning config
ProvisioningDisableControllers bool
ProvisioningAllowedTargets []string
ProvisioningRepositoryTypes []string
ProvisioningLokiURL string
ProvisioningLokiUser string
@@ -2119,6 +2120,10 @@ func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error {
}
cfg.ProvisioningDisableControllers = iniFile.Section("provisioning").Key("disable_controllers").MustBool(false)
cfg.ProvisioningAllowedTargets = iniFile.Section("provisioning").Key("allowed_targets").Strings("|")
if len(cfg.ProvisioningAllowedTargets) == 0 {
cfg.ProvisioningAllowedTargets = []string{"instance", "folder"}
}
// Read job history configuration
cfg.ProvisioningLokiURL = valueAsString(iniFile.Section("provisioning"), "loki_url", "")
@@ -4352,6 +4352,18 @@
"items"
],
"properties": {
"allowedTargets": {
"description": "The valid targets (can disable instance or folder types)",
"type": "array",
"items": {
"type": "string",
"default": "",
"enum": [
"folder",
"instance"
]
}
},
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
@@ -39,6 +39,8 @@ func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
values: map[string]any{
"SecureTokenCreate": "some-token",
"SecureWebhookSecretCreate": "some-secret",
"SyncEnabled": true,
"Target": "folder",
},
inputFile: "testdata/github-with-inline-secrets.json.tmpl",
expectedFields: []expectedField{
@@ -13,6 +13,11 @@
"branch": "{{ or .Branch "integration-test" }}",
"generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }},
"path": "{{ or .Path "grafana/" }}"
},
"sync": {
"enabled": {{ if .SyncEnabled }} true {{ else }} false {{ end }},
"target": "{{ or .Target "folder" }}",
"intervalSeconds": {{ or .SyncIntervalSeconds 60 }}
}
},
"secure": {
@@ -1468,6 +1468,8 @@ export type RepositoryView = {
workflows: ('branch' | 'write')[];
};
export type RepositoryViewList = {
/** The valid targets (can disable instance or folder types) */
allowedTargets?: ('folder' | 'instance')[];
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
apiVersion?: string;
/** AvailableRepositoryTypes is the list of repository types supported in this instance (e.g. git, bitbucket, github, etc) */
@@ -16,7 +16,11 @@ import {
Stack,
Switch,
} from '@grafana/ui';
import { Repository, useGetRepositoryRefsQuery } from 'app/api/clients/provisioning/v0alpha1';
import {
Repository,
useGetFrontendSettingsQuery,
useGetRepositoryRefsQuery,
} from 'app/api/clients/provisioning/v0alpha1';
import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt';
import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo';
@@ -32,11 +36,13 @@ import { ConfigFormGithubCollapse } from './ConfigFormGithubCollapse';
import { getDefaultValues } from './defaults';
// This needs to be a function for translations to work
const getTargetOptions = () => {
return [
const getTargetOptions = (allowedTargets: string[]) => {
const allOptions = [
{ value: 'instance', label: t('provisioning.config-form.option-entire-instance', 'Entire instance') },
{ value: 'folder', label: t('provisioning.config-form.option-managed-folder', 'Managed folder') },
];
return allOptions.filter((option) => allowedTargets.includes(option.value));
};
export interface ConfigFormProps {
@@ -44,6 +50,7 @@ export interface ConfigFormProps {
}
export function ConfigForm({ data }: ConfigFormProps) {
const repositoryName = data?.metadata?.name;
const settings = useGetFrontendSettingsQuery();
const [submitData, request] = useCreateOrUpdateRepository(repositoryName);
const {
register,
@@ -62,7 +69,10 @@ export function ConfigForm({ data }: ConfigFormProps) {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const [type, readOnly] = watch(['type', 'readOnly']);
const targetOptions = useMemo(() => getTargetOptions(), []);
const targetOptions = useMemo(
() => getTargetOptions(settings.data?.allowedTargets || ['instance', 'folder']),
[settings.data]
);
const isGitBased = isGitProvider(type);
const {
@@ -6,12 +6,17 @@ import { RepositoryViewList } from 'app/api/clients/provisioning/v0alpha1';
import { ModeOption } from '../types';
/**
* Filters available mode options based on system state
* Filters available mode options based on system state and allowed targets
*/
function filterModeOptions(modeOptions: ModeOption[], repoName: string, settings?: RepositoryViewList): ModeOption[] {
const folderConnected = settings?.items?.some((item) => item.target === 'folder' && item.name !== repoName);
const allowedTargets = settings?.allowedTargets || ['instance', 'folder'];
return modeOptions.filter((option) => {
if (!allowedTargets.includes(option.target)) {
return false;
}
if (settings?.legacyStorage) {
return option.target === 'instance';
}