Skip to content
Snippets Groups Projects
Verified Commit dd4a3c84 authored by Timo Furrer's avatar Timo Furrer :juggling:
Browse files

feat: implement environment and dashboard support

Closes #7667
parent 97e6e4ff
No related branches found
No related tags found
No related merge requests found
......@@ -60,3 +60,25 @@ var SyncFile = func(client *gitlab.Client, projectID interface{}, path string, c
})
return err
}
var CreateFile = func(client *gitlab.Client, projectID interface{}, path string, content []byte, ref string) error {
_, _, err := client.RepositoryFiles.CreateFile(projectID, path, &gitlab.CreateFileOptions{
Branch: gitlab.Ptr(ref),
Content: gitlab.Ptr(string(content)),
CommitMessage: gitlab.Ptr(fmt.Sprintf("Add %s via glab file sync", path)),
AuthorName: gitlab.Ptr(commitAuthorName),
AuthorEmail: gitlab.Ptr(commitAuthorEmail),
})
return err
}
var UpdateFile = func(client *gitlab.Client, projectID interface{}, path string, content []byte, ref string) error {
_, _, err := client.RepositoryFiles.UpdateFile(projectID, path, &gitlab.UpdateFileOptions{
Branch: gitlab.Ptr(ref),
Content: gitlab.Ptr(string(content)),
CommitMessage: gitlab.Ptr(fmt.Sprintf("Update %s via glab file sync", path)),
AuthorName: gitlab.Ptr(commitAuthorName),
AuthorEmail: gitlab.Ptr(commitAuthorEmail),
})
return err
}
......@@ -21,6 +21,8 @@ type API interface {
GetDefaultBranch() (string, error)
GetAgentByName(name string) (*gitlab.Agent, error)
RegisterAgent(name string) (*gitlab.Agent, error)
ConfigureAgent(agent *gitlab.Agent, branch string) error
ConfigureEnvironment(agentID int, name string, kubernetesNamespace string, fluxResourcePath string) error
CreateAgentToken(agentID int) (*gitlab.AgentToken, error)
SyncFile(f file, branch string) error
}
......@@ -72,6 +74,8 @@ It requires the kubectl and flux commands to be accessible via $PATH.
This command consists of multiple idempotent steps:
1. Register the agent with the project.
1. Configure the agent.
1. Configure an environment with dashboard for the agent.
1. Create a token for the agent.
- If the agent has reached the maximum amount of tokens,
the one that has not been used the longest is revoked
......@@ -92,6 +96,12 @@ glab cluster agent bootstrap my-agent --manifest-path manifests/
# Bootstrap "my-agent" to "manifests/" of Git project in CWD and do not manually trigger a reconilication
glab cluster agent bootstrap my-agent --manifest-path manifests/ --no-reconcile
# Bootstrap "my-agent" without configuring an environment
glab cluster agent bootstrap my-agent --create-environment false
# Bootstrap "my-agent" and configure an environment with custom name and Kubernetes namespace
glab cluster agent bootstrap my-agent --environment-name production --environment-namespace default
`,
Aliases: []string{"bs"},
Args: cobra.ExactArgs(1),
......@@ -183,6 +193,44 @@ glab cluster agent bootstrap my-agent --manifest-path manifests/ --no-reconcile
return err
}
createEnvironment, err := cmd.Flags().GetBool("create-environment")
if err != nil {
return err
}
var environmentCfg *environmentConfiguration
if createEnvironment {
environmentCfg = &environmentConfiguration{
name: fmt.Sprintf("%s/%s", helmReleaseNamespace, helmReleaseName),
kubernetesNamespace: helmReleaseTargetNamespace,
fluxResourcePath: fmt.Sprintf("helm.toolkit.fluxcd.io/v2beta1/namespaces/%s/helmreleases/%s", helmReleaseNamespace, helmReleaseName),
}
if cmd.Flags().Changed("environment-name") {
environmentName, err := cmd.Flags().GetString("environment-name")
if err != nil {
return err
}
environmentCfg.name = environmentName
}
if cmd.Flags().Changed("environment-namespace") {
environmentNamespace, err := cmd.Flags().GetString("environment-namespace")
if err != nil {
return err
}
environmentCfg.kubernetesNamespace = environmentNamespace
}
if cmd.Flags().Changed("environment-flux-resource-path") {
environmentFluxResourcePath, err := cmd.Flags().GetString("environment-flux-resource-path")
if err != nil {
return err
}
environmentCfg.fluxResourcePath = environmentFluxResourcePath
}
}
c := cf(stdout, stderr, os.Environ())
return (&bootstrapCmd{
......@@ -198,7 +246,8 @@ glab cluster agent bootstrap my-agent --manifest-path manifests/ --no-reconcile
helmReleaseName, helmReleaseNamespace, helmReleaseFilepath, helmReleaseTargetNamespace,
fluxSourceType, fluxSourceNamespace, fluxSourceName,
),
noReconcile: noReconcile,
noReconcile: noReconcile,
environmentCfg: environmentCfg,
}).run()
},
}
......@@ -222,6 +271,11 @@ glab cluster agent bootstrap my-agent --manifest-path manifests/ --no-reconcile
agentBootstrapCmd.Flags().String("flux-source-namespace", "flux-system", "Flux source namespace.")
agentBootstrapCmd.Flags().String("flux-source-name", "flux-system", "Flux source name.")
agentBootstrapCmd.Flags().Bool("create-environment", true, "Create an Environment for the GitLab Agent.")
agentBootstrapCmd.Flags().String("environment-name", "<helm-release-namespace>/<helm-release-name>", "Name of the Environment for the GitLab Agent.")
agentBootstrapCmd.Flags().String("environment-namespace", "<helm-release-namespace>", "Kubernetes namespace of the Environment for the GitLab Agent.")
agentBootstrapCmd.Flags().String("environment-flux-resource-path", "helm.toolkit.fluxcd.io/v2beta1/namespaces/<helm-release-namespace>/helmreleases/<helm-release-name>", "Flux Resource Path of the Environment for the GitLab Agent.")
return agentBootstrapCmd
}
......@@ -234,6 +288,7 @@ type bootstrapCmd struct {
kubectl KubectlWrapper
flux FluxWrapper
noReconcile bool
environmentCfg *environmentConfiguration
}
type file struct {
......@@ -241,16 +296,46 @@ type file struct {
content []byte
}
type environmentConfiguration struct {
name string
kubernetesNamespace string
fluxResourcePath string
}
func (c *bootstrapCmd) run() error {
// 1. Register the agent
fmt.Fprintf(c.stderr, "Registering Agent ... ")
agent, err := c.registerAgent()
if err != nil {
fmt.Fprintf(c.stderr, "[FAILED]\n")
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 2. Create a token for the registered agent
// 2. Configure the Agent
fmt.Fprintf(c.stderr, "Configuring Agent ... ")
err = c.configureAgent(agent)
if err != nil {
fmt.Fprintf(c.stderr, "[FAILED]\n")
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 3. Configure Environment for Agent
fmt.Fprintf(c.stderr, "Configuring Environment with Dashboard for Agent ... ")
if c.environmentCfg != nil {
err = c.configureEnvironment(agent)
if err != nil {
fmt.Fprintf(c.stderr, "[FAILED]\n")
return err
}
fmt.Fprintf(c.stderr, "[OK]\n")
} else {
fmt.Fprintf(c.stderr, "[SKIPPED]\n")
}
// 4. Create a token for the registered agent
fmt.Fprintf(c.stderr, "Creating Agent Token ... ")
token, err := c.createAgentToken(agent)
if err != nil {
......@@ -259,7 +344,7 @@ func (c *bootstrapCmd) run() error {
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 3. Push token in Kubernetes secret to cluster
// 5. Push token in Kubernetes secret to cluster
fmt.Fprintf(c.stderr, "Creating Kubernetes Secret with Agent Token ... ")
err = c.createAgentTokenKubernetesSecret(token)
if err != nil {
......@@ -268,7 +353,7 @@ func (c *bootstrapCmd) run() error {
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 4. Create Flux HelmRepository and HelmRelease resource.
// 6. Create Flux HelmRepository and HelmRelease resource.
fmt.Fprintf(c.stderr, "Creating Flux Helm Resources ... ")
helmResourceFiles, err := c.createFluxHelmResources()
if err != nil {
......@@ -277,7 +362,7 @@ func (c *bootstrapCmd) run() error {
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 5. Commit and Push the created Flux Helm resources to the manifest path.
// 7. Commit and Push the created Flux Helm resources to the manifest path.
fmt.Fprintf(c.stderr, "Syncing Flux Helm Resources ... ")
err = c.syncFluxHelmResourceFiles(helmResourceFiles)
if err != nil {
......@@ -286,13 +371,16 @@ func (c *bootstrapCmd) run() error {
}
fmt.Fprintf(c.stderr, "[OK]\n")
// 6. Trigger Flux reconciliation of GitLab Agent HelmRelease.
fmt.Fprintf(c.stderr, "Reconciling Flux Helm Resources ... ")
if !c.noReconcile {
// 6. Trigger Flux reconciliation of GitLab Agent HelmRelease.
fmt.Fprintln(c.stderr, "Reconciling Flux Helm Resources ... Output from flux command:")
fmt.Fprintln(c.stderr, "Output from flux command:")
err = c.fluxReconcile()
if err != nil {
return reconcileErr
}
} else {
fmt.Fprintf(c.stderr, "[SKIPPED]\n")
}
fmt.Fprintln(c.stderr, "Successfully bootstrapped the GitLab Agent")
......@@ -315,6 +403,14 @@ func (c *bootstrapCmd) registerAgent() (*gitlab.Agent, error) {
return agent, nil
}
func (c *bootstrapCmd) configureAgent(agent *gitlab.Agent) error {
return c.api.ConfigureAgent(agent, c.manifestBranch)
}
func (c *bootstrapCmd) configureEnvironment(agent *gitlab.Agent) error {
return c.api.ConfigureEnvironment(agent.ID, c.environmentCfg.name, c.environmentCfg.kubernetesNamespace, c.environmentCfg.fluxResourcePath)
}
func (c *bootstrapCmd) createAgentToken(agent *gitlab.Agent) (*gitlab.AgentToken, error) {
return c.api.CreateAgentToken(agent.ID)
}
......
This diff is collapsed.
package bootstrap
import (
"encoding/base64"
"fmt"
"slices"
"github.com/xanzy/go-gitlab"
glab_api "gitlab.com/gitlab-org/cli/api"
"gopkg.in/yaml.v3"
)
var _ API = (*apiWrapper)(nil)
......@@ -32,6 +37,96 @@ func (a *apiWrapper) RegisterAgent(name string) (*gitlab.Agent, error) {
return glab_api.RegisterAgent(a.client, a.projectID, name)
}
type agentConfig struct {
UserAccess *agentConfigUserAccess `yaml:"user_access"`
}
type agentConfigUserAccess struct {
AccessAs *agentConfigAccessAs `yaml:"access_as"`
Projects []*agentConfigProject `yaml:"projects"`
}
type agentConfigAccessAs struct {
Agent struct{}
}
type agentConfigProject struct {
ID string `yaml:"id"`
}
func (a *apiWrapper) ConfigureAgent(agent *gitlab.Agent, branch string) error {
configPath := fmt.Sprintf(".gitlab/agents/%s/config.yaml", agent.Name)
file, err := glab_api.GetFile(a.client, a.projectID, configPath, branch)
if err != nil && !glab_api.Is404(err) {
return err
}
cfg := agentConfig{}
if glab_api.Is404(err) {
cfg.UserAccess = &agentConfigUserAccess{
AccessAs: &agentConfigAccessAs{Agent: struct{}{}},
Projects: []*agentConfigProject{
{
ID: agent.ConfigProject.PathWithNamespace,
},
},
}
configuredContent, err := yaml.Marshal(cfg)
if err != nil {
return err
}
return glab_api.CreateFile(a.client, a.projectID, configPath, configuredContent, branch)
} else {
content, err := base64.StdEncoding.DecodeString(file.Content)
if err != nil {
return err
}
err = yaml.Unmarshal(content, &cfg)
if err != nil {
return err
}
if !slices.ContainsFunc(cfg.UserAccess.Projects, func(p *agentConfigProject) bool { return p.ID == agent.ConfigProject.PathWithNamespace }) {
cfg.UserAccess.Projects = append(cfg.UserAccess.Projects, &agentConfigProject{ID: agent.ConfigProject.PathWithNamespace})
}
configuredContent, err := yaml.Marshal(cfg)
if err != nil {
return err
}
return glab_api.UpdateFile(a.client, a.projectID, configPath, configuredContent, branch)
}
}
func (a *apiWrapper) ConfigureEnvironment(agentID int, name string, kubernetesNamespace string, fluxResourcePath string) error {
env, err := a.getEnvironmentByName(name)
if err != nil {
return err
}
if env == nil {
_, _, err := a.client.Environments.CreateEnvironment(a.projectID, &gitlab.CreateEnvironmentOptions{
Name: gitlab.Ptr(name),
ClusterAgentID: gitlab.Ptr(agentID),
KubernetesNamespace: gitlab.Ptr(kubernetesNamespace),
FluxResourcePath: gitlab.Ptr(fluxResourcePath),
})
return err
} else {
_, _, err := a.client.Environments.EditEnvironment(a.projectID, env.ID, &gitlab.EditEnvironmentOptions{
Name: gitlab.Ptr(name),
ClusterAgentID: gitlab.Ptr(agentID),
KubernetesNamespace: gitlab.Ptr(kubernetesNamespace),
FluxResourcePath: gitlab.Ptr(fluxResourcePath),
})
return err
}
}
func (a *apiWrapper) CreateAgentToken(agentID int) (*gitlab.AgentToken, error) {
token, _, err := glab_api.CreateAgentToken(a.client, a.projectID, agentID, true)
return token, err
......@@ -40,3 +135,23 @@ func (a *apiWrapper) CreateAgentToken(agentID int) (*gitlab.AgentToken, error) {
func (a *apiWrapper) SyncFile(f file, branch string) error {
return glab_api.SyncFile(a.client, a.projectID, f.path, f.content, branch)
}
func (a *apiWrapper) getEnvironmentByName(name string) (*gitlab.Environment, error) {
opts := &gitlab.ListEnvironmentsOptions{
Name: gitlab.Ptr(name),
}
envs, _, err := a.client.Environments.ListEnvironments(a.projectID, opts)
if err != nil {
if glab_api.Is404(err) {
return nil, nil
}
return nil, err
}
if len(envs) == 0 {
return nil, nil
}
return envs[0], nil
}
......@@ -39,6 +39,82 @@ func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
return m.recorder
}
// ConfigureAgent mocks base method.
func (m *MockAPI) ConfigureAgent(arg0 *gitlab.Agent, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigureAgent", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ConfigureAgent indicates an expected call of ConfigureAgent.
func (mr *MockAPIMockRecorder) ConfigureAgent(arg0, arg1 any) *MockAPIConfigureAgentCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureAgent", reflect.TypeOf((*MockAPI)(nil).ConfigureAgent), arg0, arg1)
return &MockAPIConfigureAgentCall{Call: call}
}
// MockAPIConfigureAgentCall wrap *gomock.Call
type MockAPIConfigureAgentCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockAPIConfigureAgentCall) Return(arg0 error) *MockAPIConfigureAgentCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockAPIConfigureAgentCall) Do(f func(*gitlab.Agent, string) error) *MockAPIConfigureAgentCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockAPIConfigureAgentCall) DoAndReturn(f func(*gitlab.Agent, string) error) *MockAPIConfigureAgentCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ConfigureEnvironment mocks base method.
func (m *MockAPI) ConfigureEnvironment(arg0 int, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigureEnvironment", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
return ret0
}
// ConfigureEnvironment indicates an expected call of ConfigureEnvironment.
func (mr *MockAPIMockRecorder) ConfigureEnvironment(arg0, arg1, arg2, arg3 any) *MockAPIConfigureEnvironmentCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureEnvironment", reflect.TypeOf((*MockAPI)(nil).ConfigureEnvironment), arg0, arg1, arg2, arg3)
return &MockAPIConfigureEnvironmentCall{Call: call}
}
// MockAPIConfigureEnvironmentCall wrap *gomock.Call
type MockAPIConfigureEnvironmentCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockAPIConfigureEnvironmentCall) Return(arg0 error) *MockAPIConfigureEnvironmentCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockAPIConfigureEnvironmentCall) Do(f func(int, string, string, string) error) *MockAPIConfigureEnvironmentCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockAPIConfigureEnvironmentCall) DoAndReturn(f func(int, string, string, string) error) *MockAPIConfigureEnvironmentCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// CreateAgentToken mocks base method.
func (m *MockAPI) CreateAgentToken(arg0 int) (*gitlab.AgentToken, error) {
m.ctrl.T.Helper()
......
......@@ -24,6 +24,8 @@ It requires the kubectl and flux commands to be accessible via $PATH.
This command consists of multiple idempotent steps:
1. Register the agent with the project.
1. Configure the agent.
1. Configure an environment with dashboard for the agent.
1. Create a token for the agent.
- If the agent has reached the maximum amount of tokens,
the one that has not been used the longest is revoked
......@@ -58,11 +60,21 @@ glab cluster agent bootstrap my-agent --manifest-path manifests/
# Bootstrap "my-agent" to "manifests/" of Git project in CWD and do not manually trigger a reconilication
glab cluster agent bootstrap my-agent --manifest-path manifests/ --no-reconcile
# Bootstrap "my-agent" without configuring an environment
glab cluster agent bootstrap my-agent --create-environment false
# Bootstrap "my-agent" and configure an environment with custom name and Kubernetes namespace
glab cluster agent bootstrap my-agent --environment-name production --environment-namespace default
```
## Options
```plaintext
--create-environment Create an Environment for the GitLab Agent. (default true)
--environment-flux-resource-path string Flux Resource Path of the Environment for the GitLab Agent. (default "helm.toolkit.fluxcd.io/v2beta1/namespaces/<helm-release-namespace>/helmreleases/<helm-release-name>")
--environment-name string Name of the Environment for the GitLab Agent. (default "<helm-release-namespace>/<helm-release-name>")
--environment-namespace string Kubernetes namespace of the Environment for the GitLab Agent. (default "<helm-release-namespace>")
--flux-source-name string Flux source name. (default "flux-system")
--flux-source-namespace string Flux source namespace. (default "flux-system")
--flux-source-type string Source type of the flux-system, e.g. git, oci, helm, ... (default "git")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment