graphql-engine/cli/commands/metadata_handlers.go

313 lines
9.0 KiB
Go
Raw Normal View History

package commands
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"github.com/hasura/graphql-engine/cli/v2/internal/hasura"
goyaml "github.com/goccy/go-yaml"
"github.com/hasura/graphql-engine/cli/v2"
"github.com/hasura/graphql-engine/cli/v2/internal/projectmetadata"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
type MetadataModeHandler interface {
Export(*MetadataExportOptions) error
Apply(*MetadataApplyOptions) error
Diff(*MetadataDiffOptions) error
}
func getMetadataModeHandler(mode cli.MetadataMode) MetadataModeHandler {
switch mode {
case cli.MetadataModeJSON:
return &metadataModeJSONHandler{}
case cli.MetadataModeYAML:
return &metadataModeYAMLHandler{}
default:
return &metadataModeDirectoryHandler{}
}
}
type metadataModeDirectoryHandler struct{}
func (m *metadataModeDirectoryHandler) Export(o *MetadataExportOptions) error {
metadataHandler := projectmetadata.NewHandlerFromEC(o.EC)
files, err := metadataHandler.ExportMetadata()
o.EC.Spinner.Stop()
if err != nil {
return errors.Wrap(err, "failed to export metadata")
}
err = metadataHandler.WriteMetadata(files)
if err != nil {
return errors.Wrap(err, "cannot write metadata to project")
}
return nil
}
func (m *metadataModeDirectoryHandler) Apply(o *MetadataApplyOptions) error {
metadataHandler := projectmetadata.NewHandlerFromEC(o.EC)
if !o.DryRun {
if o.EC.Config.Version == cli.V2 {
_, err := metadataHandler.V1ApplyMetadata()
if err != nil {
return errorApplyingMetadata(err)
}
o.EC.Logger.Debug("metadata applied using v1 replace_metadata")
} else {
r, err := metadataHandler.V2ApplyMetadata()
if err != nil {
return errorApplyingMetadata(err)
}
if !r.IsConsistent {
o.EC.Logger.Warn("Metadata is inconsistent")
}
o.EC.Logger.Debug("metadata applied using v2 replace_metadata")
}
if len(o.rawOutput) != 0 {
// if not a dry run fetch metadata from and server and print it to stdout
return getMetadataFromServerAndWriteToStdoutByFormat(o.EC, rawOutputFormat(o.rawOutput))
}
return nil
}
if o.DryRun {
projectMetadataJSON, err := metadataHandler.MakeJSONMetadata()
if err != nil {
return fmt.Errorf("error building project metadata: %w", err)
}
if o.DryRun && len(o.rawOutput) == 0 {
// ie users who probably expect old behaviour
// show a warning about change in behaviour
o.rawOutput = string(rawOutputFormatJSON)
o.EC.Logger.Warn("behaviour of --dry-run flag has changed from v2.0.0. It used to show a diff between metadata on server and local project")
o.EC.Logger.Warn("new behaviour is to output local project metadata as JSON by default. The output format is configurable by -o flag eg: `hasura metadata apply --dry-run -o yaml`")
o.EC.Logger.Warn("the old behaviour can be achieved using `hasura metadata diff` command")
}
if err := writeByOutputFormat(o.EC.Stdout, projectMetadataJSON, rawOutputFormat(o.rawOutput)); err != nil {
return fmt.Errorf("displaying metadata failed: %w", err)
}
}
return nil
}
func (m *metadataModeDirectoryHandler) Diff(o *MetadataDiffOptions) error {
args := o.Args
messageFormat := "Showing diff between %s and %s..."
message := ""
metadataHandler := projectmetadata.NewHandlerFromEC(o.EC)
from := "project"
to := "server"
switch len(args) {
case 0:
o.Metadata[0] = o.EC.MetadataDir
from = "project"
case 1:
// 1 arg, diff given directory and the metadata on server
err := checkDir(args[0])
if err != nil {
return err
}
o.Metadata[0] = args[0]
from = o.Metadata[0]
case 2:
err := checkDir(args[0])
if err != nil {
return err
}
o.Metadata[0] = args[0]
from = o.Metadata[0]
err = checkDir(args[1])
if err != nil {
return err
}
o.Metadata[1] = args[1]
to = o.Metadata[1]
}
message = fmt.Sprintf(messageFormat, from, to)
o.EC.Logger.Info(message)
var oldYaml, newYaml []byte
if o.Metadata[1] == "" {
tmpDir, err := ioutil.TempDir("", "*")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
metadataHandler.SetMetadataObjects(projectmetadata.GetMetadataObjectsWithDir(o.EC, tmpDir))
var files map[string][]byte
files, err = metadataHandler.ExportMetadata()
if err != nil {
return err
}
err = metadataHandler.WriteMetadata(files)
if err != nil {
return err
}
} else {
metadataHandler.SetMetadataObjects(projectmetadata.GetMetadataObjectsWithDir(o.EC, o.Metadata[1]))
}
// build server metadata
serverMeta, err := metadataHandler.BuildMetadata()
if err != nil {
return err
}
newYaml, err = yaml.Marshal(serverMeta)
if err != nil {
return errors.Wrap(err, "cannot unmarshall server metadata")
}
// build local metadata
metadataHandler.SetMetadataObjects(projectmetadata.GetMetadataObjectsWithDir(o.EC, o.Metadata[0]))
localMeta, err := metadataHandler.BuildMetadata()
if err != nil {
return err
}
oldYaml, err = yaml.Marshal(localMeta)
if err != nil {
return errors.Wrap(err, "cannot unmarshal local metadata")
}
// Here oldYaml is project's metadata and newYaml is server's metadata for having diff similar to git diff i.e taking server has base before has been taken as server's metadata
err = printDiff(string(newYaml), string(oldYaml), to, from, o.Output, o.DiffType, o.DisableColor)
if err != nil {
return err
}
return nil
}
type metadataModeJSONHandler struct{}
func (m *metadataModeJSONHandler) Export(o *MetadataExportOptions) error {
return export(o, o.EC.MetadataMode)
}
func (m *metadataModeJSONHandler) Apply(o *MetadataApplyOptions) error {
return apply(o, o.EC.MetadataMode)
}
func (m *metadataModeJSONHandler) Diff(o *MetadataDiffOptions) error {
return diff(o, o.EC.MetadataMode)
}
type metadataModeYAMLHandler struct{}
func (m *metadataModeYAMLHandler) Export(o *MetadataExportOptions) error {
return export(o, o.EC.MetadataMode)
}
func (m *metadataModeYAMLHandler) Apply(o *MetadataApplyOptions) error {
return apply(o, o.EC.MetadataMode)
}
func (m *metadataModeYAMLHandler) Diff(o *MetadataDiffOptions) error {
return diff(o, o.EC.MetadataMode)
}
func export(o *MetadataExportOptions, mode cli.MetadataMode) error {
metadata, err := ec.APIClient.V1Metadata.ExportMetadata()
if err != nil {
fmt.Errorf("exporting metadata from server: %w", err)
}
var metadataBytes []byte
metadataBytes, err = ioutil.ReadAll(metadata)
if err != nil {
return fmt.Errorf("reading metadata from response: %w", err)
}
if mode == cli.MetadataModeYAML {
metadataBytes, err = goyaml.JSONToYAML(metadataBytes)
if err != nil {
return fmt.Errorf("parsing metadata to yaml: %w", err)
}
}
err = ioutil.WriteFile(o.EC.MetadataFile, metadataBytes, os.ModePerm)
if err != nil {
return fmt.Errorf("writing metadata to file: %w", err)
}
return nil
}
func apply(o *MetadataApplyOptions, mode cli.MetadataMode) error {
var localMetadataBytes []byte
var err error
localMetadataBytes, err = ioutil.ReadFile(o.EC.MetadataFile)
if err != nil {
return fmt.Errorf("reading metadata file: %w", err)
}
if o.DryRun {
if len(o.rawOutput) == 0 {
o.rawOutput = string(rawOutputFormatJSON)
}
if err := writeByOutputFormat(o.EC.Stdout, localMetadataBytes, rawOutputFormat(o.rawOutput)); err != nil {
return fmt.Errorf("displaying metadata failed: %w", err)
}
return nil
}
if mode == cli.MetadataModeYAML {
localMetadataBytes, err = goyaml.YAMLToJSON(localMetadataBytes)
if err != nil {
return fmt.Errorf("parsing yaml metadata to json: %w", err)
}
}
var metadata interface{}
err = json.Unmarshal(localMetadataBytes, &metadata)
if err != nil {
return fmt.Errorf("parsing metadata as json: %w", err)
}
if o.EC.Config.Version == cli.V2 {
_, err := cli.GetCommonMetadataOps(o.EC).ReplaceMetadata(bytes.NewReader(localMetadataBytes))
if err != nil {
return err
}
return nil
}
r, err := o.EC.APIClient.V1Metadata.V2ReplaceMetadata(hasura.V2ReplaceMetadataArgs{
AllowInconsistentMetadata: true,
Metadata: metadata,
})
if err != nil {
return errorApplyingMetadata(err)
}
if !r.IsConsistent {
o.EC.Logger.Warn("Metadata is inconsistent")
}
if len(o.rawOutput) != 0 {
// if not a dry run fetch metadata from and server and print it to stdout
return getMetadataFromServerAndWriteToStdoutByFormat(o.EC, rawOutputFormat(o.rawOutput))
}
return nil
}
func diff(o *MetadataDiffOptions, mode cli.MetadataMode) error {
if len(o.Args) > 0 {
return fmt.Errorf("expected 0 arguments, found: %v", o.Args)
}
serverMetadata, err := cli.GetCommonMetadataOps(o.EC).ExportMetadata()
if err != nil {
fmt.Errorf("exporting metadata from server: %w", err)
}
var serverMetadataBytes []byte
serverMetadataBytes, err = ioutil.ReadAll(serverMetadata)
if err != nil {
fmt.Errorf("reading server metadata: %w", err)
}
if mode == cli.MetadataModeYAML {
serverMetadataBytes, err = goyaml.JSONToYAML(serverMetadataBytes)
if err != nil {
fmt.Errorf("parsing server metadata as yaml: %w", err)
}
}
localMetadataBytes, err := ioutil.ReadFile(o.EC.MetadataFile)
if err != nil {
fmt.Errorf("reading local metadata: %w", err)
}
return printDiff(string(serverMetadataBytes), string(localMetadataBytes), "server", "project", o.Output, o.DiffType, !o.EC.IsTerminal)
}