Skip to content

Commit

Permalink
feat: add support for multiple doco-cd configurations (#141)
Browse files Browse the repository at this point in the history
* feat: add support for multiple doco-cd configurations

* test: adjust commit sha for test

* fix: adjust shadowed err variable

* fix: adjust deploy config read on custom targets

* fix: adjust deploy config read on custom targets

* fix: adjust deploy config read on custom targets

* fix: adjust deploy config read on custom targets

* fix: adjust deploy config read on custom targets

* fix: adjust deploy config read on custom targets

* fix: adjust deploy config read on custom targets

* fix: show custom_target only when necessary

* fix: return error instead of default values when using custom target
  • Loading branch information
kimdre authored Sep 30, 2024
1 parent 076029d commit b87f981
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 19 deletions.
4 changes: 4 additions & 0 deletions .doco-cd.test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: compose-webhook
reference: refs/heads/main
compose_files:
- test.compose.yaml
26 changes: 16 additions & 10 deletions cmd/doco-cd/http_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ type handlerData struct {
}

// HandleEvent handles the incoming webhook event
func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter, c *config.AppConfig, p webhook.ParsedPayload, jobID string, dockerCli command.Cli) {
func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter, c *config.AppConfig, p webhook.ParsedPayload, customTarget, jobID string, dockerCli command.Cli) {
jobLog = jobLog.With(slog.String("repository", p.FullName))

if customTarget != "" {
jobLog = jobLog.With(slog.String("custom_target", customTarget))
}

jobLog.Info("preparing stack deployment")

// Clone the repository
Expand Down Expand Up @@ -86,9 +90,9 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
}

fs := worktree.Filesystem
rootDir := fs.Root()
repoDir := fs.Root()

jobLog.Debug("repository cloned", slog.String("path", rootDir))
jobLog.Debug("repository cloned", slog.String("path", repoDir))

// Defer removal of the repository
defer func(workDir string) {
Expand All @@ -104,12 +108,12 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
jobID,
http.StatusInternalServerError)
}
}(rootDir)
}(repoDir)

jobLog.Debug("retrieving deployment configuration")

// Get the deployment configs from the repository
deployConfigs, err := config.GetDeployConfigs(rootDir, p.Name)
deployConfigs, err := config.GetDeployConfigs(repoDir, p.Name, customTarget)
if err != nil {
if errors.Is(err, config.ErrDeprecatedConfig) {
jobLog.Warn(err.Error())
Expand All @@ -127,11 +131,12 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
}

for _, deployConfig := range deployConfigs {
err = deployStack(jobLog, jobID, rootDir, &w, &ctx, &dockerCli, &p, deployConfig)
err = deployStack(jobLog, repoDir, &ctx, &dockerCli, &p, deployConfig)
if err != nil {
msg := "deployment failed"
jobLog.Error(msg)
JSONError(w, err, msg, jobID, http.StatusInternalServerError)

return
}
}
Expand All @@ -144,6 +149,8 @@ func HandleEvent(ctx context.Context, jobLog *slog.Logger, w http.ResponseWriter
func (h *handlerData) WebhookHandler(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()

customTarget := r.PathValue("customTarget")

// Add job id to the context to track deployments in the logs
jobID := uuid.Must(uuid.NewRandom()).String()
jobLog := h.log.With(slog.String("job_id", jobID))
Expand Down Expand Up @@ -181,7 +188,7 @@ func (h *handlerData) WebhookHandler(w http.ResponseWriter, r *http.Request) {
return
}

HandleEvent(ctx, jobLog, w, h.appConfig, payload, jobID, h.dockerCli)
HandleEvent(ctx, jobLog, w, h.appConfig, payload, customTarget, jobID, h.dockerCli)
}

func (h *handlerData) HealthCheckHandler(w http.ResponseWriter, _ *http.Request) {
Expand All @@ -198,8 +205,7 @@ func (h *handlerData) HealthCheckHandler(w http.ResponseWriter, _ *http.Request)
}

func deployStack(
jobLog *slog.Logger, jobID, rootDir string,
w *http.ResponseWriter, ctx *context.Context,
jobLog *slog.Logger, repoDir string, ctx *context.Context,
dockerCli *command.Cli, p *webhook.ParsedPayload, deployConfig *config.DeployConfig,
) error {
stackLog := jobLog.
Expand All @@ -208,7 +214,7 @@ func deployStack(

stackLog.Debug("deployment configuration retrieved", slog.Any("config", deployConfig))

workingDir := path.Join(rootDir, deployConfig.WorkingDirectory)
workingDir := path.Join(repoDir, deployConfig.WorkingDirectory)

err := os.Chdir(workingDir)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/doco-cd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func main() {
}

http.HandleFunc(webhookPath, h.WebhookHandler)
http.HandleFunc(webhookPath+"/{customTarget}", h.WebhookHandler)

http.HandleFunc(healthPath, h.HealthCheckHandler)

Expand Down
22 changes: 22 additions & 0 deletions cmd/doco-cd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func TestHandleEvent(t *testing.T) {
expectedStatusCode int
expectedResponseBody string
overrideEnv map[string]string
customTarget string
}{
{
name: "Successful Deployment",
Expand All @@ -48,6 +49,22 @@ func TestHandleEvent(t *testing.T) {
expectedStatusCode: http.StatusCreated,
expectedResponseBody: `{"details":"deployment successful","job_id":"%s"}`,
overrideEnv: nil,
customTarget: "",
},
{
name: "Successful Deployment with custom Target",
payload: webhook.ParsedPayload{
Ref: "refs/heads/feat/128-one-repo-to-many-servers",
CommitSHA: "f291bfca73b06814293c1f9c9f3c7f95e4932564",
Name: projectName,
FullName: "kimdre/doco-cd",
CloneURL: "https://github.com/kimdre/doco-cd",
Private: false,
},
expectedStatusCode: http.StatusCreated,
expectedResponseBody: `{"details":"deployment successful","job_id":"%s"}`,
overrideEnv: nil,
customTarget: "test",
},
{
name: "Invalid Reference",
Expand All @@ -62,6 +79,7 @@ func TestHandleEvent(t *testing.T) {
expectedStatusCode: http.StatusInternalServerError,
expectedResponseBody: `{"error":"failed to clone repository","details":"couldn't find remote ref \"` + invalidBranch + `\"","job_id":"%s"}`,
overrideEnv: nil,
customTarget: "",
},
{
name: "Private Repository",
Expand All @@ -76,6 +94,7 @@ func TestHandleEvent(t *testing.T) {
expectedStatusCode: http.StatusCreated,
expectedResponseBody: `{"details":"deployment successful","job_id":"%s"}`,
overrideEnv: nil,
customTarget: "",
},
{
name: "Private Repository with missing Git Access Token",
Expand All @@ -92,6 +111,7 @@ func TestHandleEvent(t *testing.T) {
overrideEnv: map[string]string{
"GIT_ACCESS_TOKEN": "",
},
customTarget: "",
},
{
name: "Missing Deployment Configuration",
Expand All @@ -106,6 +126,7 @@ func TestHandleEvent(t *testing.T) {
expectedStatusCode: http.StatusInternalServerError,
expectedResponseBody: `{"error":"no compose files found: stat ` + filepath.Join(os.TempDir(), "kimdre/kimdre/docker-compose.yaml") + `: no such file or directory","details":"deployment failed","job_id":"%[1]s"}`,
overrideEnv: nil,
customTarget: "",
},
}

Expand Down Expand Up @@ -174,6 +195,7 @@ func TestHandleEvent(t *testing.T) {
rr,
appConfig,
tc.payload,
tc.customTarget,
jobID,
dockerCli,
)
Expand Down
24 changes: 19 additions & 5 deletions internal/config/deploy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

var (
DefaultDeploymentConfigFileNames = []string{".doco-cd.yaml", ".doco-cd.yml"}
CustomDeploymentConfigFileNames = []string{".doco-cd.%s.yaml", ".doco-cd.%s.yml"}
DeprecatedDeploymentConfigFileNames = []string{".compose-deploy.yaml", ".compose-deploy.yml"}
ErrConfigFileNotFound = errors.New("configuration file not found in repository")
ErrInvalidConfig = errors.New("invalid deploy configuration")
Expand Down Expand Up @@ -69,17 +70,26 @@ func (c *DeployConfig) validateConfig() error {
}

// GetDeployConfigs returns either the deployment configuration from the repository or the default configuration
func GetDeployConfigs(repoDir, name string) ([]*DeployConfig, error) {
func GetDeployConfigs(repoDir, name, customTarget string) ([]*DeployConfig, error) {
files, err := os.ReadDir(repoDir)
if err != nil {
return nil, err
}

// Merge default and deprecated deployment config file names
DeploymentConfigFileNames := append(DefaultDeploymentConfigFileNames, DeprecatedDeploymentConfigFileNames...)
var DeploymentConfigFileNames []string

if customTarget != "" {
for _, configFile := range CustomDeploymentConfigFileNames {
DeploymentConfigFileNames = append(DeploymentConfigFileNames, fmt.Sprintf(configFile, customTarget))
}
} else {
// Merge default and deprecated deployment config file names
DeploymentConfigFileNames = append(DefaultDeploymentConfigFileNames, DeprecatedDeploymentConfigFileNames...)
}

var configs []*DeployConfig
for _, configFile := range DeploymentConfigFileNames {
configs, err := getDeployConfigsFromFile(repoDir, files, configFile)
configs, err = getDeployConfigsFromFile(repoDir, files, configFile)
if err != nil {
if errors.Is(err, ErrConfigFileNotFound) {
continue
Expand All @@ -89,7 +99,7 @@ func GetDeployConfigs(repoDir, name string) ([]*DeployConfig, error) {
}

if configs != nil {
if err := validator.Validate(configs); err != nil {
if err = validator.Validate(configs); err != nil {
return nil, err
}

Expand All @@ -104,6 +114,10 @@ func GetDeployConfigs(repoDir, name string) ([]*DeployConfig, error) {
}
}

if customTarget != "" {
return nil, ErrConfigFileNotFound
}

return []*DeployConfig{DefaultDeployConfig(name)}, nil
}

Expand Down
8 changes: 5 additions & 3 deletions internal/config/deploy_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func TestGetDeployConfigs(t *testing.T) {
reference := "refs/heads/test"
workingDirectory := "/test"
composeFiles := []string{"test.compose.yaml"}
customTarget := ""

deployConfig := fmt.Sprintf(`name: %s
reference: %s
Expand All @@ -58,7 +59,7 @@ compose_files:
t.Fatal(err)
}

configs, err := GetDeployConfigs(dirName, projectName)
configs, err := GetDeployConfigs(dirName, projectName, customTarget)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -91,6 +92,7 @@ compose_files:
reference := "refs/heads/test"
workingDirectory := "/test"
composeFiles := []string{"test.compose.yaml"}
customTarget := ""

deployConfig := fmt.Sprintf(`name: %s
reference: %s
Expand All @@ -114,7 +116,7 @@ compose_files:
t.Fatal(err)
}

configs, err := GetDeployConfigs(dirName, projectName)
configs, err := GetDeployConfigs(dirName, projectName, customTarget)
if err == nil || !errors.Is(err, ErrDeprecatedConfig) {
t.Fatalf("expected deprecated config error, got %v", err)
}
Expand Down Expand Up @@ -159,7 +161,7 @@ func TestGetDeployConfigs_DefaultValues(t *testing.T) {
}
})

configs, err := GetDeployConfigs(dirName, projectName)
configs, err := GetDeployConfigs(dirName, projectName, "")
if err != nil {
t.Fatal(err)
}
Expand Down
3 changes: 2 additions & 1 deletion internal/docker/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func TestDeployCompose(t *testing.T) {
reference := "refs/heads/test"
workingDirectory := "/test"
composeFiles := []string{"test.compose.yaml"}
customTarget := ""

deployConfig := fmt.Sprintf(`name: %s
reference: %s
Expand All @@ -163,7 +164,7 @@ compose_files:
t.Fatal(err)
}

deployConfigs, err := config.GetDeployConfigs(dirName, projectName)
deployConfigs, err := config.GetDeployConfigs(dirName, projectName, customTarget)
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit b87f981

Please sign in to comment.