fix: reconcile generated bootstrap state
This commit is contained in:
parent
8ca8e88ca0
commit
d9707a1f85
|
|
@ -19,6 +19,7 @@ var bootstrapPromptDemocraticCSI bool
|
|||
var bootstrapPromptOperationalSecrets bool
|
||||
var bootstrapInitializeOpenBaoRecovery bool
|
||||
var bootstrapPublishAppFrom string
|
||||
var bootstrapMergeBootstrapPR bool
|
||||
|
||||
var bootstrapCmd = &cobra.Command{
|
||||
Use: "bootstrap",
|
||||
|
|
@ -36,11 +37,25 @@ func init() {
|
|||
bootstrapCmd.Flags().BoolVar(&bootstrapPromptOperationalSecrets, "prompt-operational-secrets", false, "Prompt for and encrypt operational secrets for --config")
|
||||
bootstrapCmd.Flags().BoolVar(&bootstrapInitializeOpenBaoRecovery, "initialize-openbao-recovery", false, "Create and save a separate OpenBao recovery age identity for --config")
|
||||
bootstrapCmd.Flags().StringVar(&bootstrapPublishAppFrom, "publish-app-from", "", "Push this app checkout's current branch and create a Forgejo delivery PR")
|
||||
bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping")
|
||||
}
|
||||
|
||||
func runBootstrap(cmd *cobra.Command, args []string) error {
|
||||
var cfg config.Config
|
||||
var err error
|
||||
if bootstrapMergeBootstrapPR {
|
||||
if bootstrapConfigPath == "" {
|
||||
return fmt.Errorf("--merge-bootstrap-pr requires --config")
|
||||
}
|
||||
cfg, err = config.Load(bootstrapConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manager := forgejo.NewRepoManager(cfg.Git.BaseURL, cfg.Git.Token, cfg.Git.Owner, cfg.Git.Username, "", "", cfg.Flux.Branch, "")
|
||||
if err := manager.MergePullRequest(cfg.Flux.RepoName, "maidn/bootstrap-"+cfg.ClusterID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if bootstrapPublishAppFrom != "" {
|
||||
if bootstrapConfigPath == "" {
|
||||
return fmt.Errorf("--publish-app-from requires --config")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -251,6 +252,9 @@ func writeDemocraticCSISecret(path string, csi config.DemocraticCSIConfig, ageKe
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing, err := decryptSOPSFile(path, ageKeyPath); err == nil && sameYAML(existing, plaintext) {
|
||||
return nil
|
||||
}
|
||||
return writeSOPSEncryptedFile(path, ageKeyPath, plaintext)
|
||||
}
|
||||
|
||||
|
|
@ -289,6 +293,18 @@ func writeSOPSEncryptedFile(path, ageKeyPath string, plaintext []byte) error {
|
|||
return os.WriteFile(path, encrypted, 0600)
|
||||
}
|
||||
|
||||
func decryptSOPSFile(path, ageKeyPath string) ([]byte, error) {
|
||||
command := exec.Command("sops", "--decrypt", "--output-type", "yaml", path)
|
||||
command.Env = append(os.Environ(), "SOPS_AGE_KEY_FILE="+ageKeyPath)
|
||||
return command.Output()
|
||||
}
|
||||
|
||||
func sameYAML(left, right []byte) bool {
|
||||
var leftValue any
|
||||
var rightValue any
|
||||
return yaml.Unmarshal(left, &leftValue) == nil && yaml.Unmarshal(right, &rightValue) == nil && reflect.DeepEqual(leftValue, rightValue)
|
||||
}
|
||||
|
||||
func renderDemocraticCSISecret(csi config.DemocraticCSIConfig) ([]byte, error) {
|
||||
return yaml.Marshal(struct {
|
||||
APIVersion string `yaml:"apiVersion"`
|
||||
|
|
|
|||
|
|
@ -82,6 +82,16 @@ func TestWriteDemocraticCSISecretEncryptsValues(t *testing.T) {
|
|||
if strings.Contains(string(encrypted), csi.TrueNASAPIKey) || !strings.Contains(string(encrypted), "sops:") {
|
||||
t.Fatal("Democratic CSI secret was not SOPS encrypted")
|
||||
}
|
||||
if err := writeDemocraticCSISecret(secretPath, csi, identity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unchanged, err := os.ReadFile(secretPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(encrypted) != string(unchanged) {
|
||||
t.Fatal("Democratic CSI secret was re-encrypted without a configuration change")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWebhookAuthorization(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ type pullRequestRequest struct {
|
|||
Base string `json:"base"`
|
||||
}
|
||||
|
||||
type pullRequest struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
type mergePullRequestRequest struct {
|
||||
Do string `json:"Do"`
|
||||
}
|
||||
|
||||
type hook struct {
|
||||
ID int64 `json:"id"`
|
||||
URL string `json:"url"`
|
||||
|
|
@ -193,6 +201,31 @@ func (rm *RepoManager) CreatePullRequest(repo, title, head, base string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (rm *RepoManager) MergePullRequest(repo, head string) error {
|
||||
values := url.Values{"state": {"open"}, "head": {head}}
|
||||
endpoint := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls?%s", rm.BaseURL, rm.Owner, repo, values.Encode())
|
||||
var pullRequests []pullRequest
|
||||
status, err := rm.apiJSONRequest(http.MethodGet, endpoint, nil, &pullRequests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != http.StatusOK || len(pullRequests) != 1 {
|
||||
return fmt.Errorf("expected one open Forgejo pull request for branch %q", head)
|
||||
}
|
||||
body, err := json.Marshal(mergePullRequestRequest{Do: "merge"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, err = rm.apiRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/merge", rm.BaseURL, rm.Owner, repo, pullRequests[0].Index), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Errorf("unexpected Forgejo pull request merge status %d", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rm *RepoManager) PushBranch(dir, repoURL, branch string) error {
|
||||
return rm.PushRef(dir, repoURL, "HEAD", branch)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,32 @@ func TestHasRemoteBranchReturnsFalseForMissingBranch(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMergePullRequest(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
switch request.Method {
|
||||
case http.MethodGet:
|
||||
_ = json.NewEncoder(writer).Encode([]pullRequest{{Index: 4}})
|
||||
case http.MethodPost:
|
||||
var body mergePullRequestRequest
|
||||
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Do != "merge" || request.URL.Path != "/api/v1/repos/owner/cluster/pulls/4/merge" {
|
||||
t.Fatalf("unexpected merge request")
|
||||
}
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", request.Method)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test")
|
||||
manager.HTTPClient = server.Client()
|
||||
if err := manager.MergePullRequest("cluster", "maidn/bootstrap-test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoExistsReturnsFalseOnNotFound(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
|
|
|
|||
Loading…
Reference in a new issue