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:
co-authored by
Ryan McKinley
Alex Khomenko
parent
0ab7488305
commit
cb11bc15fa
@@ -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)",
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user