mirror of
https://github.com/grafana/grafana.git
synced 2026-01-15 23:27:12 +00:00
Compare commits
3 Commits
gamab/auth
...
build-dock
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d3172a1a2 | ||
|
|
65cdf6cd45 | ||
|
|
7be93d9af4 |
14
Dockerfile
14
Dockerfile
@@ -118,7 +118,7 @@ COPY pkg/codegen pkg/codegen
|
||||
COPY pkg/plugins/codegen pkg/plugins/codegen
|
||||
COPY apps/example apps/example
|
||||
|
||||
RUN go mod download
|
||||
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
||||
|
||||
COPY embed.go Makefile build.go package.json ./
|
||||
COPY cue.mod cue.mod
|
||||
@@ -135,7 +135,9 @@ COPY .github .github
|
||||
ENV COMMIT_SHA=${COMMIT_SHA}
|
||||
ENV BUILD_BRANCH=${BUILD_BRANCH}
|
||||
|
||||
RUN make build-go GO_BUILD_TAGS=${GO_BUILD_TAGS} WIRE_TAGS=${WIRE_TAGS}
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
make build-go GO_BUILD_TAGS=${GO_BUILD_TAGS} WIRE_TAGS=${WIRE_TAGS}
|
||||
|
||||
# From-tarball build stage
|
||||
FROM ${BASE_IMAGE} AS tgz-builder
|
||||
@@ -168,7 +170,8 @@ ENV PATH="/usr/share/grafana/bin:$PATH" \
|
||||
GF_PATHS_HOME="/usr/share/grafana" \
|
||||
GF_PATHS_LOGS="/var/log/grafana" \
|
||||
GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
|
||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
|
||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning" \
|
||||
GF_UNIFIED_STORAGE_INDEX_PATH="/var/lib/grafana-search/bleve"
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
@@ -225,13 +228,14 @@ RUN if [ ! $(getent group "$GF_GID") ]; then \
|
||||
"$GF_PATHS_PROVISIONING/plugins" \
|
||||
"$GF_PATHS_PROVISIONING/access-control" \
|
||||
"$GF_PATHS_PROVISIONING/alerting" \
|
||||
"$GF_UNIFIED_STORAGE_INDEX_PATH" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp conf/sample.ini "$GF_PATHS_CONFIG" && \
|
||||
cp conf/ldap.toml /etc/grafana/ldap.toml && \
|
||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" "$GF_UNIFIED_STORAGE_INDEX_PATH" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" "$GF_UNIFIED_STORAGE_INDEX_PATH"
|
||||
|
||||
COPY --from=go-src /tmp/grafana/bin/grafana* /tmp/grafana/bin/*/grafana* ./bin/
|
||||
COPY --from=js-src /tmp/grafana/public ./public
|
||||
|
||||
18
Makefile
18
Makefile
@@ -135,7 +135,7 @@ i18n-extract-enterprise:
|
||||
@echo "Skipping i18n extract for Enterprise: not enabled"
|
||||
else
|
||||
i18n-extract-enterprise:
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
cd public/locales/enterprise && yarn run i18next-cli extract --sync-primary
|
||||
endif
|
||||
|
||||
@@ -418,6 +418,11 @@ shellcheck: $(SH_FILES) ## Run checks for shell scripts.
|
||||
TAG_SUFFIX=$(if $(WIRE_TAGS)!=oss,-$(WIRE_TAGS))
|
||||
PLATFORM=linux/amd64
|
||||
|
||||
# JS_SRC can be set to a pre-built JS image to skip frontend build
|
||||
# Example: make build-docker-full JS_SRC=grafana-js-cache:latest
|
||||
JS_SRC ?=
|
||||
DOCKER_JS_SRC_ARG = $(if $(JS_SRC),--build-arg JS_SRC=$(JS_SRC))
|
||||
|
||||
# default to a production build for frontend
|
||||
#
|
||||
DOCKER_JS_NODE_ENV_FLAG = production
|
||||
@@ -436,13 +441,20 @@ ifeq (${NODE_ENV}, dev)
|
||||
DOCKER_JS_YARN_BUILD_FLAG = dev
|
||||
DOCKER_JS_YARN_INSTALL_FLAG =
|
||||
endif
|
||||
.PHONY: build-docker-js-cache
|
||||
build-docker-js-cache: ## Build and cache the frontend Docker image for faster subsequent builds.
|
||||
@echo "building JS cache image"
|
||||
docker buildx build . \
|
||||
--platform $(PLATFORM) \
|
||||
--target js-builder \
|
||||
--tag grafana-js-cache:latest
|
||||
|
||||
.PHONY: build-docker-full
|
||||
build-docker-full: ## Build Docker image for development.
|
||||
@echo "build docker container mode=($(DOCKER_JS_NODE_ENV_FLAG))"
|
||||
tar -ch . | \
|
||||
docker buildx build - \
|
||||
docker buildx build . \
|
||||
--platform $(PLATFORM) \
|
||||
$(DOCKER_JS_SRC_ARG) \
|
||||
--build-arg NODE_ENV=$(DOCKER_JS_NODE_ENV_FLAG) \
|
||||
--build-arg JS_NODE_ENV=$(DOCKER_JS_NODE_ENV_FLAG) \
|
||||
--build-arg JS_YARN_INSTALL_FLAG=$(DOCKER_JS_YARN_INSTALL_FLAG) \
|
||||
|
||||
@@ -116,3 +116,26 @@ type ConnectionList struct {
|
||||
// +listType=atomic
|
||||
Items []Connection `json:"items"`
|
||||
}
|
||||
|
||||
// ExternalRepositoryList lists repositories from an external git provider
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ExternalRepositoryList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// +listType=atomic
|
||||
Items []ExternalRepository `json:"items"`
|
||||
}
|
||||
|
||||
type ExternalRepository struct {
|
||||
// Name of the repository
|
||||
Name string `json:"name"`
|
||||
// Owner is the user, organization, or workspace that owns the repository
|
||||
// For GitHub: organization or user
|
||||
// For GitLab: namespace (user or group)
|
||||
// For Bitbucket: workspace
|
||||
// For pure Git: empty
|
||||
Owner string `json:"owner,omitempty"`
|
||||
// URL of the repository
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
|
||||
&HistoricJobList{},
|
||||
&Connection{},
|
||||
&ConnectionList{},
|
||||
&ExternalRepositoryList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -262,6 +262,53 @@ func (in *ExportJobOptions) DeepCopy() *ExportJobOptions {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExternalRepository) DeepCopyInto(out *ExternalRepository) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalRepository.
|
||||
func (in *ExternalRepository) DeepCopy() *ExternalRepository {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ExternalRepository)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExternalRepositoryList) DeepCopyInto(out *ExternalRepositoryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ExternalRepository, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalRepositoryList.
|
||||
func (in *ExternalRepositoryList) DeepCopy() *ExternalRepositoryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ExternalRepositoryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ExternalRepositoryList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FileItem) DeepCopyInto(out *FileItem) {
|
||||
*out = *in
|
||||
|
||||
@@ -26,6 +26,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.DeleteJobOptions": schema_pkg_apis_provisioning_v0alpha1_DeleteJobOptions(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ErrorDetails": schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExportJobOptions": schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository": schema_pkg_apis_provisioning_v0alpha1_ExternalRepository(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepositoryList": schema_pkg_apis_provisioning_v0alpha1_ExternalRepositoryList(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubConnectionConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubConnectionConfig(ref),
|
||||
@@ -544,6 +546,96 @@ func schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref common.Reference
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_ExternalRepository(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Name of the repository",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"owner": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Owner is the user, organization, or workspace that owns the repository For GitHub: organization or user For GitLab: namespace (user or group) For Bitbucket: workspace For pure Git: empty",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"url": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "URL of the repository",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "url"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_ExternalRepositoryList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ExternalRepositoryList lists repositories from an external git provider",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
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{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_FileItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Paths
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Resources
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ExternalRepositoryList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,FileList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,HistoryList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobResourceSummary,Errors
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"@faker-js/faker": "^9.8.0",
|
||||
"@grafana/api-clients": "12.4.0-pre",
|
||||
"@grafana/i18n": "12.4.0-pre",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@reduxjs/toolkit": "2.10.1",
|
||||
"fishery": "^2.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"tinycolor2": "^1.6.0"
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grafana/runtime": ">=11.6 <= 12.x",
|
||||
"@reduxjs/toolkit": "^2.8.0",
|
||||
"@reduxjs/toolkit": "^2.10.0",
|
||||
"rxjs": "7.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@ const injectedRtkApi = api
|
||||
}),
|
||||
invalidatesTags: ['Connection'],
|
||||
}),
|
||||
getConnectionRepositories: build.query<GetConnectionRepositoriesApiResponse, GetConnectionRepositoriesApiArg>({
|
||||
query: (queryArg) => ({ url: `/connections/${queryArg.name}/repositories` }),
|
||||
providesTags: ['Connection'],
|
||||
}),
|
||||
getConnectionStatus: build.query<GetConnectionStatusApiResponse, GetConnectionStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/connections/${queryArg.name}/status`,
|
||||
@@ -726,6 +730,18 @@ export type UpdateConnectionApiArg = {
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type GetConnectionRepositoriesApiResponse = /** status 200 OK */ {
|
||||
/** 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;
|
||||
items: any[];
|
||||
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
||||
kind?: string;
|
||||
metadata?: any;
|
||||
};
|
||||
export type GetConnectionRepositoriesApiArg = {
|
||||
/** name of the ExternalRepositoryList */
|
||||
name: string;
|
||||
};
|
||||
export type GetConnectionStatusApiResponse = /** status 200 OK */ Connection;
|
||||
export type GetConnectionStatusApiArg = {
|
||||
/** name of the Connection */
|
||||
@@ -2079,6 +2095,8 @@ export const {
|
||||
useReplaceConnectionMutation,
|
||||
useDeleteConnectionMutation,
|
||||
useUpdateConnectionMutation,
|
||||
useGetConnectionRepositoriesQuery,
|
||||
useLazyGetConnectionRepositoriesQuery,
|
||||
useGetConnectionStatusQuery,
|
||||
useLazyGetConnectionStatusQuery,
|
||||
useReplaceConnectionStatusMutation,
|
||||
|
||||
69
pkg/registry/apis/provisioning/connection_repositories.go
Normal file
69
pkg/registry/apis/provisioning/connection_repositories.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
type connectionRepositoriesConnector struct{}
|
||||
|
||||
func NewConnectionRepositoriesConnector() *connectionRepositoriesConnector {
|
||||
return &connectionRepositoriesConnector{}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) New() runtime.Object {
|
||||
return &provisioning.ExternalRepositoryList{}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) Destroy() {}
|
||||
|
||||
func (*connectionRepositoriesConnector) ProducesMIMETypes(verb string) []string {
|
||||
return []string{"application/json"}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) ProducesObject(verb string) any {
|
||||
return &provisioning.ExternalRepositoryList{}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) ConnectMethods() []string {
|
||||
return []string{http.MethodGet}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
func (c *connectionRepositoriesConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
logger := logging.FromContext(ctx).With("logger", "connection-repositories-connector", "connection_name", name)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
responder.Error(apierrors.NewMethodNotSupported(provisioning.ConnectionResourceInfo.GroupResource(), r.Method))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("repositories endpoint called but not yet implemented")
|
||||
|
||||
// TODO: Implement repository listing from external git provider
|
||||
// This will require:
|
||||
// 1. Get the Connection object using logging.Context(r.Context(), logger)
|
||||
// 2. Use the connection credentials to authenticate with the git provider
|
||||
// 3. List repositories from the provider (GitHub, GitLab, Bitbucket)
|
||||
// 4. Return ExternalRepositoryList with Name, Owner, and URL for each repository
|
||||
|
||||
responder.Error(apierrors.NewMethodNotSupported(provisioning.ConnectionResourceInfo.GroupResource(), "repositories endpoint not yet implemented"))
|
||||
}), nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ rest.Storage = (*connectionRepositoriesConnector)(nil)
|
||||
_ rest.Connecter = (*connectionRepositoriesConnector)(nil)
|
||||
_ rest.StorageMetadata = (*connectionRepositoriesConnector)(nil)
|
||||
)
|
||||
101
pkg/registry/apis/provisioning/connection_repositories_test.go
Normal file
101
pkg/registry/apis/provisioning/connection_repositories_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
func TestConnectionRepositoriesConnector(t *testing.T) {
|
||||
connector := NewConnectionRepositoriesConnector()
|
||||
|
||||
t.Run("New returns ExternalRepositoryList", func(t *testing.T) {
|
||||
obj := connector.New()
|
||||
require.IsType(t, &provisioning.ExternalRepositoryList{}, obj)
|
||||
})
|
||||
|
||||
t.Run("ProducesMIMETypes returns application/json", func(t *testing.T) {
|
||||
types := connector.ProducesMIMETypes("GET")
|
||||
require.Equal(t, []string{"application/json"}, types)
|
||||
})
|
||||
|
||||
t.Run("ProducesObject returns ExternalRepositoryList", func(t *testing.T) {
|
||||
obj := connector.ProducesObject("GET")
|
||||
require.IsType(t, &provisioning.ExternalRepositoryList{}, obj)
|
||||
})
|
||||
|
||||
t.Run("ConnectMethods returns GET", func(t *testing.T) {
|
||||
methods := connector.ConnectMethods()
|
||||
require.Equal(t, []string{http.MethodGet}, methods)
|
||||
})
|
||||
|
||||
t.Run("NewConnectOptions returns no path component", func(t *testing.T) {
|
||||
obj, hasPath, path := connector.NewConnectOptions()
|
||||
require.Nil(t, obj)
|
||||
require.False(t, hasPath)
|
||||
require.Empty(t, path)
|
||||
})
|
||||
|
||||
t.Run("Connect returns handler that rejects non-GET methods", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
responder := &mockResponder{}
|
||||
|
||||
handler, err := connector.Connect(ctx, "test-connection", nil, responder)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
// Test POST method (should be rejected)
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.True(t, responder.called)
|
||||
require.NotNil(t, responder.err)
|
||||
require.True(t, apierrors.IsMethodNotSupported(responder.err))
|
||||
})
|
||||
|
||||
t.Run("Connect returns handler that returns not implemented for GET", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
responder := &mockResponder{}
|
||||
|
||||
handler, err := connector.Connect(ctx, "test-connection", nil, responder)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
// Test GET method (should return not implemented)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.True(t, responder.called)
|
||||
require.NotNil(t, responder.err)
|
||||
require.True(t, apierrors.IsMethodNotSupported(responder.err))
|
||||
require.Contains(t, responder.err.Error(), "not yet implemented")
|
||||
})
|
||||
}
|
||||
|
||||
// mockResponder implements rest.Responder for testing
|
||||
type mockResponder struct {
|
||||
called bool
|
||||
err error
|
||||
obj runtime.Object
|
||||
code int
|
||||
}
|
||||
|
||||
func (m *mockResponder) Object(statusCode int, obj runtime.Object) {
|
||||
m.called = true
|
||||
m.code = statusCode
|
||||
m.obj = obj
|
||||
}
|
||||
|
||||
func (m *mockResponder) Error(err error) {
|
||||
m.called = true
|
||||
m.err = err
|
||||
}
|
||||
@@ -480,6 +480,16 @@ func (b *APIBuilder) authorizeConnectionSubresource(ctx context.Context, a autho
|
||||
Namespace: a.GetNamespace(),
|
||||
}, ""))
|
||||
|
||||
// Repositories is read-only
|
||||
case "repositories":
|
||||
return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{
|
||||
Verb: apiutils.VerbGet,
|
||||
Group: provisioning.GROUP,
|
||||
Resource: provisioning.ConnectionResourceInfo.GetName(),
|
||||
Name: a.GetName(),
|
||||
Namespace: a.GetNamespace(),
|
||||
}, ""))
|
||||
|
||||
default:
|
||||
id, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
@@ -603,6 +613,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
|
||||
|
||||
storage[provisioning.ConnectionResourceInfo.StoragePath()] = connectionsStore
|
||||
storage[provisioning.ConnectionResourceInfo.StoragePath("status")] = connectionStatusStorage
|
||||
storage[provisioning.ConnectionResourceInfo.StoragePath("repositories")] = NewConnectionRepositoriesConnector()
|
||||
|
||||
// TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b, repository.NewRepositoryTesterWithExistingChecker(repository.NewSimpleRepositoryTester(b.validator), b.VerifyAgainstExistingRepositories))
|
||||
@@ -1247,6 +1258,23 @@ spec:
|
||||
oas.Paths.Paths[repoprefix+"/jobs/{uid}"] = sub
|
||||
}
|
||||
|
||||
// Document connection repositories endpoint
|
||||
connectionprefix := root + "namespaces/{namespace}/connections/{name}"
|
||||
sub = oas.Paths.Paths[connectionprefix+"/repositories"]
|
||||
if sub != nil {
|
||||
sub.Get.Description = "List repositories available from the external git provider through this connection"
|
||||
sub.Get.Summary = "List external repositories"
|
||||
sub.Get.Parameters = []*spec3.Parameter{}
|
||||
sub.Post = nil
|
||||
sub.Put = nil
|
||||
sub.Delete = nil
|
||||
|
||||
// Replace the content type for this response
|
||||
mt := sub.Get.Responses.StatusCodeResponses[200].Content
|
||||
s := defs[defsBase+"ExternalRepositoryList"].Schema
|
||||
mt["*/*"].Schema = &s
|
||||
}
|
||||
|
||||
// Run all extra post-processors.
|
||||
for _, extra := range b.extras {
|
||||
if err := extra.PostProcessOpenAPI(oas); err != nil {
|
||||
|
||||
@@ -866,6 +866,80 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/repositories": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Connection"
|
||||
],
|
||||
"summary": "List external repositories",
|
||||
"description": "List repositories available from the external git provider through this connection",
|
||||
"operationId": "getConnectionRepositories",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"description": "ExternalRepositoryList lists repositories from an external git provider",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"items"
|
||||
],
|
||||
"properties": {
|
||||
"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"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"default": {}
|
||||
},
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"default": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "connect",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "provisioning.grafana.app",
|
||||
"version": "v0alpha1",
|
||||
"kind": "ExternalRepositoryList"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "name of the ExternalRepositoryList",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"description": "object name and auth scope, such as for teams and projects",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -4645,6 +4719,73 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepository": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the repository",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"owner": {
|
||||
"description": "Owner is the user, organization, or workspace that owns the repository For GitHub: organization or user For GitLab: namespace (user or group) For Bitbucket: workspace For pure Git: empty",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL of the repository",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepositoryList": {
|
||||
"description": "ExternalRepositoryList lists repositories from an external git provider",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"items"
|
||||
],
|
||||
"properties": {
|
||||
"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"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"default": {},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepository"
|
||||
}
|
||||
]
|
||||
},
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"default": {},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "provisioning.grafana.app",
|
||||
"kind": "ExternalRepositoryList",
|
||||
"version": "v0alpha1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.FileItem": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
172
pkg/tests/apis/provisioning/connection_repositories_test.go
Normal file
172
pkg/tests/apis/provisioning/connection_repositories_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
func TestIntegrationProvisioning_ConnectionRepositories(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
|
||||
// Create a connection for testing
|
||||
connection := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "connection-repositories-test",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"type": "github",
|
||||
"github": map[string]any{
|
||||
"appID": "123456",
|
||||
"installationID": "454545",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"privateKey": map[string]any{
|
||||
"create": "someSecret",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
|
||||
require.NoError(t, err, "failed to create connection")
|
||||
|
||||
t.Run("endpoint returns not implemented", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.AdminREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).
|
||||
StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "should return error for not implemented endpoint")
|
||||
require.Equal(t, http.StatusMethodNotAllowed, statusCode, "should return 405 Method Not Allowed")
|
||||
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
|
||||
})
|
||||
|
||||
t.Run("admin can access endpoint (gets not implemented)", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.AdminREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
// Endpoint exists but returns not implemented
|
||||
require.Error(t, result.Error(), "should return error")
|
||||
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
|
||||
// Status code should be 405 (Method Not Allowed) for method not supported
|
||||
require.Equal(t, http.StatusMethodNotAllowed, statusCode)
|
||||
})
|
||||
|
||||
t.Run("editor cannot access endpoint", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.EditorREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "editor should not be able to access repositories endpoint")
|
||||
require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden")
|
||||
require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden")
|
||||
})
|
||||
|
||||
t.Run("viewer cannot access endpoint", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.ViewerREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "viewer should not be able to access repositories endpoint")
|
||||
require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden")
|
||||
require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden")
|
||||
})
|
||||
|
||||
t.Run("non-GET methods are rejected", func(t *testing.T) {
|
||||
configBytes, _ := json.Marshal(map[string]any{})
|
||||
|
||||
var statusCode int
|
||||
result := helper.AdminREST.Post().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Body(configBytes).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "POST should not be allowed")
|
||||
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_ConnectionRepositoriesResponseType(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
|
||||
// Create a connection for testing
|
||||
connection := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "connection-repositories-type-test",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"type": "github",
|
||||
"github": map[string]any{
|
||||
"appID": "123456",
|
||||
"installationID": "454545",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"privateKey": map[string]any{
|
||||
"create": "someSecret",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
|
||||
require.NoError(t, err, "failed to create connection")
|
||||
|
||||
t.Run("verify ExternalRepositoryList type exists in API", func(t *testing.T) {
|
||||
// Verify the type is registered and can be instantiated
|
||||
list := &provisioning.ExternalRepositoryList{}
|
||||
require.NotNil(t, list)
|
||||
// Verify it has the expected structure (Items is a slice, nil by default is fine)
|
||||
require.IsType(t, []provisioning.ExternalRepository{}, list.Items)
|
||||
// Can create items
|
||||
list.Items = []provisioning.ExternalRepository{
|
||||
{Name: "test", Owner: "owner", URL: "https://example.com/repo"},
|
||||
}
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, "test", list.Items[0].Name)
|
||||
})
|
||||
}
|
||||
@@ -3009,7 +3009,7 @@ __metadata:
|
||||
"@grafana/api-clients": "npm:12.4.0-pre"
|
||||
"@grafana/i18n": "npm:12.4.0-pre"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
"@reduxjs/toolkit": "npm:^2.9.0"
|
||||
"@reduxjs/toolkit": "npm:2.10.1"
|
||||
"@testing-library/jest-dom": "npm:^6.6.3"
|
||||
"@testing-library/react": "npm:^16.3.0"
|
||||
"@testing-library/user-event": "npm:^14.6.1"
|
||||
@@ -3052,7 +3052,7 @@ __metadata:
|
||||
typescript: "npm:5.9.2"
|
||||
peerDependencies:
|
||||
"@grafana/runtime": ">=11.6 <= 12.x"
|
||||
"@reduxjs/toolkit": ^2.8.0
|
||||
"@reduxjs/toolkit": ^2.10.0
|
||||
rxjs: 7.8.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -7294,7 +7294,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reduxjs/toolkit@npm:2.10.1, @reduxjs/toolkit@npm:^2.9.0":
|
||||
"@reduxjs/toolkit@npm:2.10.1":
|
||||
version: 2.10.1
|
||||
resolution: "@reduxjs/toolkit@npm:2.10.1"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user