Skip to content

Commit

Permalink
feat(cli): add plan and apply command for provider & policy (#129)
Browse files Browse the repository at this point in the history
* feat(cli): add apply command for policy & provider

* feat(cli): add plan command for policy & provider

* chore(cli): enhance diff formatting

* chore(cmd): omit provider credentials from diff
  • Loading branch information
rahmatrhd authored Feb 2, 2022
1 parent fdcdd54 commit 22725bd
Show file tree
Hide file tree
Showing 4 changed files with 354 additions and 0 deletions.
169 changes: 169 additions & 0 deletions cmd/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"strconv"
"strings"
"time"

"github.com/MakeNowJust/heredoc"
handlerv1beta1 "github.com/odpf/guardian/api/handler/v1beta1"
Expand All @@ -16,6 +17,9 @@ import (
"github.com/odpf/salt/printer"
"github.com/odpf/salt/term"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gopkg.in/yaml.v3"
)

// PolicyCmd is the root command for the policies subcommand.
Expand Down Expand Up @@ -44,6 +48,8 @@ func PolicyCmd(c *app.CLIConfig, adapter handlerv1beta1.ProtoAdapter) *cobra.Com
cmd.AddCommand(getPolicyCmd(c, adapter))
cmd.AddCommand(createPolicyCmd(c, adapter))
cmd.AddCommand(updatePolicyCmd(c, adapter))
cmd.AddCommand(planPolicyCmd(c, adapter))
cmd.AddCommand(applyPolicyCmd(c, adapter))
cmd.AddCommand(initPolicyCmd(c))

return cmd
Expand Down Expand Up @@ -327,6 +333,169 @@ func initPolicyCmd(c *app.CLIConfig) *cobra.Command {

return cmd
}

func applyPolicyCmd(c *app.CLIConfig, adapter handlerv1beta1.ProtoAdapter) *cobra.Command {
var filePath string

cmd := &cobra.Command{
Use: "apply",
Short: "Apply a policy config",
Long: heredoc.Doc(`
Create or edit a policy from a file.
`),
Example: heredoc.Doc(`
$ guardian policy apply --file=<file-path>
`),
Annotations: map[string]string{
"group:core": "true",
},
RunE: func(cmd *cobra.Command, args []string) error {
spinner := printer.Spin("")
defer spinner.Stop()

var policy domain.Policy
if err := parseFile(filePath, &policy); err != nil {
return err
}

policyProto, err := adapter.ToPolicyProto(&policy)
if err != nil {
return err
}

ctx := context.Background()
client, cancel, err := createClient(ctx, c.Host)
if err != nil {
return err
}
defer cancel()

policyID := policyProto.GetId()
_, err = client.GetPolicy(ctx, &guardianv1beta1.GetPolicyRequest{
Id: policyID,
})
policyExists := true
if err != nil {
if e, ok := status.FromError(err); ok && e.Code() == codes.NotFound {
policyExists = false
} else {
return err
}
}

if policyExists {
res, err := client.UpdatePolicy(ctx, &guardianv1beta1.UpdatePolicyRequest{
Id: policyID,
Policy: policyProto,
})
if err != nil {
return err
}
spinner.Stop()

fmt.Printf("Policy updated to version: %v\n", res.GetPolicy().GetVersion())
} else {
res, err := client.CreatePolicy(ctx, &guardianv1beta1.CreatePolicyRequest{
Policy: policyProto,
})
if err != nil {
return err
}
spinner.Stop()

fmt.Printf("Policy created with id: %v\n", res.GetPolicy().GetId())
}

return nil
},
}

cmd.Flags().StringVarP(&filePath, "file", "f", "", "Path to the policy config")
cmd.MarkFlagRequired("file")

return cmd
}

func planPolicyCmd(c *app.CLIConfig, adapter handlerv1beta1.ProtoAdapter) *cobra.Command {
var filePath string

cmd := &cobra.Command{
Use: "plan",
Short: "Show changes from the new policy",
Long: heredoc.Doc(`
Show changes from the new policy. This will not actually apply the policy config.
`),
Example: heredoc.Doc(`
$ guardian policy plan --file=<file-path>
`),
Annotations: map[string]string{
"group:core": "true",
},
RunE: func(cmd *cobra.Command, args []string) error {
spinner := printer.Spin("")
defer spinner.Stop()

var newPolicy domain.Policy
if err := parseFile(filePath, &newPolicy); err != nil {
return err
}

policyProto, err := adapter.ToPolicyProto(&newPolicy)
if err != nil {
return err
}

ctx := context.Background()
client, cancel, err := createClient(ctx, c.Host)
if err != nil {
return err
}
defer cancel()

policyID := policyProto.GetId()
res, err := client.GetPolicy(ctx, &guardianv1beta1.GetPolicyRequest{
Id: policyID,
})
if err != nil {
return err
}

existingPolicy, err := adapter.FromPolicyProto(res.GetPolicy())
if err != nil {
return fmt.Errorf("unable to parse existing policy: %w", err)
}
if existingPolicy != nil {
newPolicy.Version = existingPolicy.Version + 1
newPolicy.CreatedAt = existingPolicy.CreatedAt
} else {
newPolicy.Version = 1
newPolicy.CreatedAt = time.Now()
}
newPolicy.UpdatedAt = time.Now()

existingPolicyYaml, err := yaml.Marshal(existingPolicy)
if err != nil {
return fmt.Errorf("failed to marshal existing policy: %w", err)
}
newPolicyYaml, err := yaml.Marshal(newPolicy)
if err != nil {
return fmt.Errorf("failed to marshal new policy: %w", err)
}

diffs := diff(string(existingPolicyYaml), string(newPolicyYaml))

spinner.Stop()
fmt.Println(diffs)
return nil
},
}

cmd.Flags().StringVarP(&filePath, "file", "f", "", "Path to the policy config")
cmd.MarkFlagRequired("file")

return cmd
}

func getVersion(versionFlag string, policyId []string) (uint64, error) {
if versionFlag != "" {
ver, err := strconv.ParseUint(versionFlag, 10, 32)
Expand Down
172 changes: 172 additions & 0 deletions cmd/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/odpf/guardian/domain"
"github.com/odpf/salt/printer"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

func ProviderCmd(c *app.CLIConfig, adapter handlerv1beta1.ProtoAdapter) *cobra.Command {
Expand Down Expand Up @@ -40,6 +41,8 @@ func ProviderCmd(c *app.CLIConfig, adapter handlerv1beta1.ProtoAdapter) *cobra.C
cmd.AddCommand(viewProviderCmd(c, adapter))
cmd.AddCommand(createProviderCmd(c, adapter))
cmd.AddCommand(editProviderCmd(c, adapter))
cmd.AddCommand(planProviderCmd(c, adapter))
cmd.AddCommand(applyProviderCmd(c, adapter))
cmd.AddCommand(initProviderCmd(c))

return cmd
Expand Down Expand Up @@ -292,3 +295,172 @@ func initProviderCmd(c *app.CLIConfig) *cobra.Command {

return cmd
}

func applyProviderCmd(c *app.CLIConfig, adapter handlerv1beta1.ProtoAdapter) *cobra.Command {
var filePath string
cmd := &cobra.Command{
Use: "apply",
Short: "Apply a provider",
Long: heredoc.Doc(`
Create or edit a provider from a file.
`),
Example: heredoc.Doc(`
$ guardian provider apply --file <file-path>
`),
Annotations: map[string]string{
"group:core": "true",
},
RunE: func(cmd *cobra.Command, args []string) error {
spinner := printer.Spin("")
defer spinner.Stop()

var providerConfig domain.ProviderConfig
if err := parseFile(filePath, &providerConfig); err != nil {
return err
}

configProto, err := adapter.ToProviderConfigProto(&providerConfig)
if err != nil {
return err
}

ctx := context.Background()
client, cancel, err := createClient(ctx, c.Host)
if err != nil {
return err
}
defer cancel()

pType := configProto.GetType()
pUrn := configProto.GetUrn()

listRes, err := client.ListProviders(ctx, &guardianv1beta1.ListProvidersRequest{}) // TODO: filter by type & urn
if err != nil {
return err
}
providerID := ""
for _, p := range listRes.GetProviders() {
if p.GetType() == pType && p.GetUrn() == pUrn {
providerID = p.GetId()
}
}

if providerID == "" {
res, err := client.CreateProvider(ctx, &guardianv1beta1.CreateProviderRequest{
Config: configProto,
})
if err != nil {
return err
}

spinner.Stop()
fmt.Printf("Provider created with id: %v", res.GetProvider().GetId())
} else {
_, err = client.UpdateProvider(ctx, &guardianv1beta1.UpdateProviderRequest{
Id: providerID,
Config: configProto,
})
if err != nil {
return err
}

spinner.Stop()
fmt.Println("Successfully updated provider")
}

return nil
},
}

cmd.Flags().StringVarP(&filePath, "file", "f", "", "Path to the provider config")
cmd.MarkFlagRequired("file")

return cmd
}

func planProviderCmd(c *app.CLIConfig, adapter handlerv1beta1.ProtoAdapter) *cobra.Command {
var filePath string
cmd := &cobra.Command{
Use: "plan",
Short: "Show changes from the new provider",
Long: heredoc.Doc(`
Show changes from the new provider. This will not actually apply the provider config.
`),
Example: heredoc.Doc(`
$ guardian provider plan --file=<file-path>
`),
Annotations: map[string]string{
"group:core": "true",
},
RunE: func(cmd *cobra.Command, args []string) error {
spinner := printer.Spin("")
defer spinner.Stop()

var newProvider domain.ProviderConfig
if err := parseFile(filePath, &newProvider); err != nil {
return err
}

ctx := context.Background()
client, cancel, err := createClient(ctx, c.Host)
if err != nil {
return err
}
defer cancel()

pType := newProvider.Type
pUrn := newProvider.URN

listRes, err := client.ListProviders(ctx, &guardianv1beta1.ListProvidersRequest{}) // TODO: filter by type & urn
if err != nil {
return err
}
providerID := ""
for _, p := range listRes.GetProviders() {
if p.GetType() == pType && p.GetUrn() == pUrn {
providerID = p.GetId()
}
}

var existingProvider *domain.ProviderConfig
if providerID != "" {
getRes, err := client.GetProvider(ctx, &guardianv1beta1.GetProviderRequest{
Id: providerID,
})
if err != nil {
return err
}

pc, err := adapter.FromProviderConfigProto(getRes.GetProvider().GetConfig())
if err != nil {
return fmt.Errorf("unable to parse existing provider: %w", err)
}
existingProvider = pc
}

existingProvider.Credentials = nil
newProvider.Credentials = nil
// TODO: show decrypted credentials value instead of omitting them

existingProviderYaml, err := yaml.Marshal(existingProvider)
if err != nil {
return fmt.Errorf("failed to marshal existing provider: %w", err)
}
newProviderYaml, err := yaml.Marshal(newProvider)
if err != nil {
return fmt.Errorf("failed to marshal new provider: %w", err)
}

diffs := diff(string(existingProviderYaml), string(newProviderYaml))

spinner.Stop()
fmt.Println(diffs)
return nil
},
}

cmd.Flags().StringVarP(&filePath, "file", "f", "", "Path to the provider config")
cmd.MarkFlagRequired("file")

return cmd
}
Loading

0 comments on commit 22725bd

Please sign in to comment.