Skip to content

Commit

Permalink
feat: rework command structure (#215)
Browse files Browse the repository at this point in the history
* feat: rework command structure

Signed-off-by: Bence Csati <bcsati@cisco.com>

* chore: adjust build path

Signed-off-by: Bence Csati <bcsati@cisco.com>

* chore: code-review

Signed-off-by: Bence Csati <bcsati@cisco.com>

* feat: use struct for params

Signed-off-by: Bence Csati <bcsati@cisco.com>

---------

Signed-off-by: Bence Csati <bcsati@cisco.com>
  • Loading branch information
csatib02 authored Jul 2, 2024
1 parent 3d4bed4 commit ab57ef0
Show file tree
Hide file tree
Showing 17 changed files with 252 additions and 235 deletions.
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ builds:
- env:
- CGO_ENABLED=0
ldflags: "-s -w -X main.Version={{ .Version }}"
main: ./cmd/
main: .
goos:
- linux
- darwin
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ RUN go mod download

COPY . .

RUN go build -o /usr/local/bin/secret-sync ./cmd/
RUN go build -o /usr/local/bin/secret-sync .
RUN xx-verify /usr/local/bin/secret-sync


Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ down: ## Destroy development environment
.PHONY: build
build: ## Build binary
@mkdir -p build
go build -race -o build/secret-sync ./cmd/
go build -race -o build/

.PHONY: container-image
container-image: ## Build container image
Expand Down
32 changes: 21 additions & 11 deletions cmd/main.go → cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package main
package cmd

import (
"context"
Expand All @@ -24,24 +24,34 @@ import (

slogmulti "github.com/samber/slog-multi"
slogsyslog "github.com/samber/slog-syslog"
"github.com/spf13/cobra"

"github.com/bank-vaults/secret-sync/cmd/sync"
"github.com/bank-vaults/secret-sync/pkg/config"
)

var Version = "v0.1.3"

func main() {
config := config.LoadConfig()

initLogger(config)
var rootCmd = &cobra.Command{
Use: "secret-sync",
Long: `Secret Sync exposes a generic way to interact with external secret storage systems
like HashiCorp Vault and provides a set of API models
to interact and orchestrate the synchronization of secrets between them.`,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
}

syncCMD := sync.NewSyncCmd(context.Background())
if err := syncCMD.ExecuteContext(syncCMD.Context()); err != nil {
slog.ErrorContext(syncCMD.Context(), fmt.Errorf("error executing command: %w", err).Error())
func Execute() {
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
slog.ErrorContext(rootCmd.Context(), fmt.Sprintf("failed to execute command: %v", err))
os.Exit(1)
}
}

func init() {
cobra.OnInitialize(func() {
initLogger(config.LoadConfig())
})
}

func initLogger(config *config.Config) {
var level slog.Level

Expand Down
196 changes: 196 additions & 0 deletions cmd/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright © 2024 Bank-Vaults Maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"encoding/json"
"fmt"
"log/slog"
"os"
"os/signal"

"github.com/ghodss/yaml"
"github.com/krayzpipes/cronticker/cronticker"
"github.com/spf13/cobra"

"github.com/bank-vaults/secret-sync/pkg/apis/v1alpha1"
"github.com/bank-vaults/secret-sync/pkg/provider"
"github.com/bank-vaults/secret-sync/pkg/storesync"
)

const (
flagSource = "source"
flagTarget = "target"
flagSyncJob = "syncjob"
flagSchedule = "schedule"
)

var syncCmdParams = struct {
SourceStorePath string
TargetStorePath string
SyncJobPath string
Schedule string
}{}

type syncJob struct {
source *v1alpha1.StoreClient
target *v1alpha1.StoreClient
syncPlan *v1alpha1.SyncPlan
}

var syncCmd = &cobra.Command{
Use: "sync",
Short: "Synchronizes secrets from a source to a target store based on sync strategy.",
RunE: run,
}

func init() {
rootCmd.AddCommand(syncCmd)
syncCmd.PersistentFlags().StringVarP(&syncCmdParams.SourceStorePath, flagSource, "s", "", "Source store config file.")
_ = syncCmd.MarkPersistentFlagRequired(flagSource)
syncCmd.PersistentFlags().StringVarP(&syncCmdParams.TargetStorePath, flagTarget, "t", "", "Target store config file. ")
_ = syncCmd.MarkPersistentFlagRequired(flagTarget)
syncCmd.PersistentFlags().StringVar(&syncCmdParams.SyncJobPath, flagSyncJob, "", "Sync job config file. ")
_ = syncCmd.MarkPersistentFlagRequired(flagSyncJob)

syncCmd.PersistentFlags().StringVar(&syncCmdParams.Schedule, flagSchedule, "", "Sync periodically using CRON schedule. If not specified, runs only once.")
}

func run(cmd *cobra.Command, args []string) error {
syncJob, err := prepareSync(cmd, args)
if err != nil {
return fmt.Errorf("failed to prepare sync job: %w", err)
}

// Run once
if syncJob.syncPlan.GetSchedule(cmd.Root().Context()) == nil {
resp, err := storesync.Sync(cmd.Root().Context(), *syncJob.source, *syncJob.target, syncJob.syncPlan.SyncAction)
if err != nil {
return fmt.Errorf("failed to sync secrets: %w", err)
}
slog.InfoContext(cmd.Root().Context(), resp.Status)

return nil
}

// Run on schedule
cronTicker, err := cronticker.NewTicker(syncJob.syncPlan.Schedule)
if err != nil {
return fmt.Errorf("failed to create CRON ticker: %w", err)
}
defer cronTicker.Stop()

cancel := make(chan os.Signal, 1)
signal.Notify(cancel, os.Interrupt)
for {
select {
case <-cronTicker.C:
slog.InfoContext(cmd.Root().Context(), "Handling a new sync request...")

resp, err := storesync.Sync(cmd.Root().Context(), *syncJob.source, *syncJob.target, syncJob.syncPlan.SyncAction)
if err != nil {
return err
}
slog.InfoContext(cmd.Root().Context(), resp.Status)

case <-cancel:
return nil
}
}
}

func prepareSync(cmd *cobra.Command, _ []string) (*syncJob, error) {
// Init source
sourceStore, err := loadStore(syncCmdParams.SourceStorePath)
if err != nil {
return nil, fmt.Errorf("failed to load source store: %w", err)
}

sourceProvider, err := provider.NewClient(cmd.Root().Context(), sourceStore)
if err != nil {
return nil, fmt.Errorf("failed to create source client: %w", err)
}

// Init target
targetStore, err := loadStore(syncCmdParams.TargetStorePath)
if err != nil {
return nil, fmt.Errorf("failed to load target store: %w", err)
}

targetProvider, err := provider.NewClient(cmd.Root().Context(), targetStore)
if err != nil {
return nil, fmt.Errorf("failed to create target client: %w", err)
}

// Init sync request by loading from file and overriding from cli
syncPlan, err := loadSyncPlan(syncCmdParams.SyncJobPath)
if err != nil {
return nil, fmt.Errorf("failed to load sync plan: %w", err)
}

syncPlan.Schedule = syncCmdParams.Schedule

return &syncJob{
source: &sourceProvider,
target: &targetProvider,
syncPlan: syncPlan,
}, nil
}

func loadStore(path string) (*v1alpha1.SecretStoreSpec, error) {
// Load file
yamlBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}

// Unmarshal (convert YAML to JSON)
var storeConfig = struct {
SecretsStore v1alpha1.SecretStoreSpec `json:"secretsStore"`
}{}

jsonBytes, err := yaml.YAMLToJSON(yamlBytes)
if err != nil {
return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err)
}

if err := json.Unmarshal(jsonBytes, &storeConfig); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}

return &storeConfig.SecretsStore, nil
}

func loadSyncPlan(path string) (*v1alpha1.SyncPlan, error) {
// Load file
yamlBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}

// Unmarshal (convert YAML to JSON)
var ruleCfg v1alpha1.SyncPlan

jsonBytes, err := yaml.YAMLToJSON(yamlBytes)
if err != nil {
return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err)
}

if err := json.Unmarshal(jsonBytes, &ruleCfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}

return &ruleCfg, nil
}
Loading

0 comments on commit ab57ef0

Please sign in to comment.