diff --git a/cli/internal/common.go b/cli/internal/common.go index af198ceb..9222a23a 100644 --- a/cli/internal/common.go +++ b/cli/internal/common.go @@ -132,3 +132,130 @@ var DNSPriorityFlag = &cli.IntFlag{ Usage: "Domain name priority value", Destination: &DNSPriority, } + +var ServerProvider string + +var ServerProviderFlag = &cli.StringFlag{ + Name: "provider", + Aliases: []string{"p"}, + Usage: "3rd party server provider", + Destination: &ServerProvider, +} + +var CapsulInstanceURL string + +var CapsulInstanceURLFlag = &cli.StringFlag{ + Name: "capsul-url", + Value: "", + Aliases: []string{"cu"}, + Usage: "Capsul instance URL", + Destination: &CapsulInstanceURL, +} + +var CapsulName string + +var CapsulNameFlag = &cli.StringFlag{ + Name: "capsul-name", + Value: "", + Aliases: []string{"cn"}, + Usage: "Capsul name", + Destination: &CapsulName, +} + +var CapsulType string + +var CapsulTypeFlag = &cli.StringFlag{ + Name: "capsul-type", + Value: "", + Aliases: []string{"ct"}, + Usage: "Capsul type", + Destination: &CapsulType, +} + +var CapsulImage string + +var CapsulImageFlag = &cli.StringFlag{ + Name: "capsul-image", + Value: "debian10", + Aliases: []string{"ci"}, + Usage: "Capsul image", + Destination: &CapsulImage, +} + +var CapsulSSHKeys cli.StringSlice + +var CapsulSSHKeysFlag = &cli.StringSliceFlag{ + Name: "capsul-ssh-keys", + Aliases: []string{"cs"}, + Usage: "Capsul SSH key (e.g. me@foo.com)", + Destination: &CapsulSSHKeys, +} + +var CapsulAPIToken string + +var CapsulAPITokenFlag = &cli.StringFlag{ + Name: "capsul-token", + Aliases: []string{"ca"}, + Usage: "Capsul API token", + EnvVars: []string{"CAPSUL_TOKEN"}, + Destination: &CapsulAPIToken, +} + +var HetznerCloudName string + +var HetznerCloudNameFlag = &cli.StringFlag{ + Name: "hetzner-name", + Value: "", + Aliases: []string{"hn"}, + Usage: "hetzner cloud name", + Destination: &HetznerCloudName, +} + +var HetznerCloudType string + +var HetznerCloudTypeFlag = &cli.StringFlag{ + Name: "hetzner-type", + Aliases: []string{"ht"}, + Usage: "hetzner cloud type", + Destination: &HetznerCloudType, + Value: "cx11", +} + +var HetznerCloudImage string + +var HetznerCloudImageFlag = &cli.StringFlag{ + Name: "hetzner-image", + Aliases: []string{"hi"}, + Usage: "hetzner cloud image", + Value: "debian-10", + Destination: &HetznerCloudImage, +} + +var HetznerCloudSSHKeys cli.StringSlice + +var HetznerCloudSSHKeysFlag = &cli.StringSliceFlag{ + Name: "hetzner-ssh-keys", + Aliases: []string{"hs"}, + Usage: "hetzner cloud SSH keys (e.g. me@foo.com)", + Destination: &HetznerCloudSSHKeys, +} + +var HetznerCloudLocation string + +var HetznerCloudLocationFlag = &cli.StringFlag{ + Name: "hetzner-location", + Aliases: []string{"hl"}, + Usage: "hetzner cloud server location", + Value: "hel1", + Destination: &HetznerCloudLocation, +} + +var HetznerCloudAPIToken string + +var HetznerCloudAPITokenFlag = &cli.StringFlag{ + Name: "hetzner-token", + Aliases: []string{"ha"}, + Usage: "hetzner cloud API token", + EnvVars: []string{"HCLOUD_TOKEN"}, + Destination: &HetznerCloudAPIToken, +} diff --git a/cli/internal/server.go b/cli/internal/server.go new file mode 100644 index 00000000..db8d8806 --- /dev/null +++ b/cli/internal/server.go @@ -0,0 +1,191 @@ +package internal + +import ( + "fmt" + + "github.com/AlecAivazis/survey/v2" + "github.com/urfave/cli/v2" +) + +// EnsureServerProvider ensures a 3rd party server provider is chosen. +func EnsureServerProvider() error { + if ServerProvider == "" && !NoInput { + prompt := &survey.Select{ + Message: "Select server provider", + Options: []string{"capsul", "hetzner-cloud"}, + } + + if err := survey.AskOne(prompt, &ServerProvider); err != nil { + return err + } + } + + if ServerProvider == "" { + return fmt.Errorf("missing server provider?") + } + + return nil +} + +// EnsureCapsulNewFlags ensure all flags are present. +func EnsureNewCapsulVPSFlags(c *cli.Context) error { + if CapsulName == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify capsul name", + } + if err := survey.AskOne(prompt, &CapsulName); err != nil { + return err + } + } + + if CapsulInstanceURL == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify capsul instance URL", + Default: "yolo.servers.coop", + } + if err := survey.AskOne(prompt, &CapsulInstanceURL); err != nil { + return err + } + } + + if CapsulType == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify capsul type", + Default: "f1-xs", + } + if err := survey.AskOne(prompt, &CapsulType); err != nil { + return err + } + } + + if CapsulImage == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify capsul image", + Default: "debian10", + } + if err := survey.AskOne(prompt, &CapsulImage); err != nil { + return err + } + } + + if len(CapsulSSHKeys.Value()) == 0 && !NoInput { + prompt := &survey.Input{ + Message: "specify capsul SSH keys", + Default: "me@foo.com,you@bar.com", + } + if err := survey.AskOne(prompt, &CapsulSSHKeys); err != nil { + return err + } + } + + if CapsulAPIToken == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify capsul API token", + } + if err := survey.AskOne(prompt, &CapsulAPIToken); err != nil { + return err + } + } + + if CapsulName == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul name?")) + } + if CapsulInstanceURL == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul instance url?")) + } + if CapsulType == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul type?")) + } + if CapsulImage == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul image?")) + } + if len(CapsulSSHKeys.Value()) == 0 { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul ssh keys?")) + } + if CapsulAPIToken == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul API token?")) + } + + return nil +} + +// EnsureNewHetznerCloudVPSFlags ensure all flags are present. +func EnsureNewHetznerCloudVPSFlags(c *cli.Context) error { + if HetznerCloudName == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify hetzner cloud VPS name", + } + if err := survey.AskOne(prompt, &HetznerCloudName); err != nil { + return err + } + } + + if HetznerCloudType == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify hetzner cloud VPS type", + Default: "cx11", + } + if err := survey.AskOne(prompt, &HetznerCloudType); err != nil { + return err + } + } + + if HetznerCloudImage == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify hetzner cloud VPS image", + Default: "debian-10", + } + if err := survey.AskOne(prompt, &HetznerCloudImage); err != nil { + return err + } + } + + if len(HetznerCloudSSHKeys.Value()) == 0 && !NoInput { + prompt := &survey.Input{ + Message: "specify hetzner cloud SSH keys", + Default: "me@foo.com,you@bar.com", + } + if err := survey.AskOne(prompt, &HetznerCloudSSHKeys); err != nil { + return err + } + } + + if HetznerCloudLocation == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify hetzner cloud VPS location", + } + if err := survey.AskOne(prompt, &HetznerCloudLocation); err != nil { + return err + } + } + + if HetznerCloudAPIToken == "" && !NoInput { + prompt := &survey.Input{ + Message: "specify hetzner cloud API token", + } + if err := survey.AskOne(prompt, &HetznerCloudAPIToken); err != nil { + return err + } + } + + if HetznerCloudName == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS name?")) + } + if HetznerCloudType == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS type?")) + } + if HetznerCloudImage == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud image?")) + } + if len(HetznerCloudSSHKeys.Value()) == 0 { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud ssh keys?")) + } + if HetznerCloudLocation == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS location?")) + } + if HetznerCloudAPIToken == "" { + ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud API token?")) + } + + return nil +} diff --git a/cli/record/new.go b/cli/record/new.go index 1fa87af8..3b25cc2b 100644 --- a/cli/record/new.go +++ b/cli/record/new.go @@ -15,8 +15,8 @@ import ( "github.com/urfave/cli/v2" ) -// RecordCreateCommand lists domains. -var RecordCreateCommand = &cli.Command{ +// RecordNewCommand creates a new domain name record. +var RecordNewCommand = &cli.Command{ Name: "new", Usage: "Create a new domain record", Aliases: []string{"n"}, diff --git a/cli/record/record.go b/cli/record/record.go index df62e813..622be875 100644 --- a/cli/record/record.go +++ b/cli/record/record.go @@ -23,16 +23,16 @@ You need an account with such a provider already. Typically, you need to provide an API token on the Abra command-line when using these commands so that you can authenticate with your provider account. -Any new provider can be integrated, we welcome change sets. See the underlying -DNS library documentation for more. It supports many existing providers and -allows to implement new provider support easily. +New providers can be integrated, we welcome change sets. See the underlying DNS +library documentation for more. It supports many existing providers and allows +to implement new provider support easily. https://pkg.go.dev/github.com/libdns/libdns `, Subcommands: []*cli.Command{ RecordListCommand, - RecordCreateCommand, + RecordNewCommand, RecordRemoveCommand, }, } diff --git a/cli/server/new.go b/cli/server/new.go index b89c3034..47d013ed 100644 --- a/cli/server/new.go +++ b/cli/server/new.go @@ -1,207 +1,148 @@ package server import ( - "context" - "errors" "fmt" + "strings" "coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/libcapsul" + "github.com/AlecAivazis/survey/v2" "github.com/hetznercloud/hcloud-go/hcloud" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) -var hetznerCloudType string -var hetznerCloudImage string -var hetznerCloudSSHKeys cli.StringSlice -var hetznerCloudLocation string -var hetznerCloudAPIToken string -var serverNewHetznerCloudCommand = &cli.Command{ - Name: "hetzner", - Usage: "Create a new Hetzner virtual server", - ArgsUsage: "", - Description: ` -Create a new Hetzner virtual server. +func newHetznerCloudVPS(c *cli.Context) error { + if err := internal.EnsureNewHetznerCloudVPSFlags(c); err != nil { + return err + } -This command uses the uses the Hetzner Cloud API bindings to send a server -creation request. You must already have a Hetzner Cloud account and an account -API token before using this command. + client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken)) -Your token can be loaded from the environment using the HCLOUD_TOKEN -environment variable or otherwise passing the "--token/-T" flag. -`, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Usage: "Server type", - Destination: &hetznerCloudType, - Value: "cx11", - }, - &cli.StringFlag{ - Name: "image", - Aliases: []string{"i"}, - Usage: "Image type", - Value: "debian-10", - Destination: &hetznerCloudImage, - }, - &cli.StringSliceFlag{ - Name: "ssh-keys", - Aliases: []string{"s"}, - Usage: "SSH keys", - Destination: &hetznerCloudSSHKeys, - }, - &cli.StringFlag{ - Name: "location", - Aliases: []string{"l"}, - Usage: "Server location", - Value: "hel1", - Destination: &hetznerCloudLocation, - }, - &cli.StringFlag{ - Name: "token", - Aliases: []string{"T"}, - Usage: "Hetzner Cloud API token", - EnvVars: []string{"HCLOUD_TOKEN"}, - Destination: &hetznerCloudAPIToken, - }, - }, - Action: func(c *cli.Context) error { - name := c.Args().First() - if name == "" { - internal.ShowSubcommandHelpAndError(c, errors.New("no name provided")) - } - - if hetznerCloudAPIToken == "" { - logrus.Fatal("Hetzner Cloud API token is missing") - } - - ctx := context.Background() - client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken)) - - logrus.Debugf("successfully created hetzner cloud API client") - - var sshKeys []*hcloud.SSHKey - for _, sshKey := range c.StringSlice("ssh-keys") { - sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey) - if err != nil { - logrus.Fatal(err) - } - sshKeys = append(sshKeys, sshKey) - } - - serverOpts := hcloud.ServerCreateOpts{ - Name: name, - ServerType: &hcloud.ServerType{Name: hetznerCloudType}, - Image: &hcloud.Image{Name: hetznerCloudImage}, - SSHKeys: sshKeys, - Location: &hcloud.Location{Name: hetznerCloudLocation}, - } - res, _, err := client.Server.Create(ctx, serverOpts) + var sshKeysRaw []string + var sshKeys []*hcloud.SSHKey + for _, sshKey := range c.StringSlice("ssh-keys") { + sshKey, _, err := client.SSHKey.GetByName(c.Context, sshKey) if err != nil { - logrus.Fatal(err) + return err } + sshKeys = append(sshKeys, sshKey) + sshKeysRaw = append(sshKeysRaw, sshKey.Name) + } - logrus.Debugf("new server '%s' created", name) + serverOpts := hcloud.ServerCreateOpts{ + Name: internal.HetznerCloudName, + ServerType: &hcloud.ServerType{Name: internal.HetznerCloudType}, + Image: &hcloud.Image{Name: internal.HetznerCloudImage}, + SSHKeys: sshKeys, + Location: &hcloud.Location{Name: internal.HetznerCloudLocation}, + } - tableColumns := []string{"Name", "IPv4", "Root Password"} - table := formatter.CreateTable(tableColumns) + tableColumns := []string{"name", "type", "image", "ssh-keys", "location"} + table := formatter.CreateTable(tableColumns) + table.Append([]string{ + internal.HetznerCloudName, + internal.HetznerCloudType, + internal.HetznerCloudImage, + strings.Join(sshKeysRaw, "\n"), + internal.HetznerCloudLocation, + }) + table.Render() - if len(sshKeys) > 0 { - table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"}) - } else { - table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword}) - } + response := false + prompt := &survey.Confirm{ + Message: "continue with capsul creation?", + } - table.Render() + if err := survey.AskOne(prompt, &response); err != nil { + return err + } - return nil - }, + if !response { + logrus.Fatal("exiting as requested") + } + + res, _, err := client.Server.Create(c.Context, serverOpts) + if err != nil { + return err + } + + tableColumns = []string{"name", "ipv4", "root password"} + table = formatter.CreateTable(tableColumns) + if len(sshKeys) > 0 { + table.Append([]string{ + internal.HetznerCloudName, + res.Server.PublicNet.IPv4.IP.String(), + "N/A (using SSH keys)", + }) + } else { + table.Append([]string{ + internal.HetznerCloudName, + res.Server.PublicNet.IPv4.IP.String(), + res.RootPassword, + }) + } + table.SetCaption(true, "hetzner cloud creation response") + table.Render() + + return nil } -var capsulInstance string -var capsulType string -var capsulImage string -var capsulSSHKey string -var capsulAPIToken string -var serverNewCapsulCommand = &cli.Command{ - Name: "capsul", - Usage: "Create a new Capsul virtual server", - ArgsUsage: "", - Description: ` -Create a new Capsul virtual server. +func newCapsulVPS(c *cli.Context) error { + if err := internal.EnsureNewCapsulVPSFlags(c); err != nil { + return err + } -This command uses the uses the Capsul API bindings of your chosen instance to -send a server creation request. You must already have an account on your chosen -Capsul instance before using this command. + capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", internal.CapsulInstanceURL) -Your token can be loaded from the environment using the CAPSUL_TOKEN -environment variable or otherwise passing the "--token/-T" flag. -`, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "instance", - Aliases: []string{"I"}, - Usage: "Capsul instance", - Destination: &capsulInstance, - Value: "yolo.servers.coop", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Usage: "Server type", - Value: "f1-xs", - Destination: &capsulType, - }, - &cli.StringFlag{ - Name: "image", - Aliases: []string{"i"}, - Usage: "Image type", - Value: "debian10", - Destination: &capsulImage, - }, - &cli.StringFlag{ - Name: "ssh-key", - Aliases: []string{"s"}, - Usage: "SSH key", - Value: "", - Destination: &capsulSSHKey, - }, - &cli.StringFlag{ - Name: "token", - Aliases: []string{"T"}, - Usage: "Capsul instance API token", - EnvVars: []string{"CAPSUL_TOKEN"}, - Destination: &capsulAPIToken, - }, - }, - Action: func(c *cli.Context) error { - capsulName := c.Args().First() - if capsulName == "" { - internal.ShowSubcommandHelpAndError(c, errors.New("no name provided")) - } + var sshKeys []string + for _, sshKey := range c.StringSlice("capsul-ssh-keys") { + sshKeys = append(sshKeys, sshKey) + } - if capsulAPIToken == "" { - logrus.Fatal("Capsul API token is missing") - } + tableColumns := []string{"instance", "name", "type", "image", "ssh-keys"} + table := formatter.CreateTable(tableColumns) + table.Append([]string{ + internal.CapsulInstanceURL, + internal.CapsulName, + internal.CapsulType, + internal.CapsulImage, + strings.Join(sshKeys, "\n"), + }) + table.Render() - capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance) + response := false + prompt := &survey.Confirm{ + Message: "continue with capsul creation?", + } - capsulClient := libcapsul.New(capsulCreateURL, capsulAPIToken) - resp, err := capsulClient.Create(capsulName, capsulType, capsulImage, capsulSSHKey) - if err != nil { - logrus.Fatal(err) - } + if err := survey.AskOne(prompt, &response); err != nil { + return err + } - tableColumns := []string{"Name", "ID"} - table := formatter.CreateTable(tableColumns) - table.Append([]string{capsulName, resp.ID}) - table.Render() + if !response { + logrus.Fatal("exiting as requested") + } - return nil - }, + capsulClient := libcapsul.New(capsulCreateURL, internal.CapsulAPIToken) + resp, err := capsulClient.Create( + internal.CapsulName, + internal.CapsulType, + internal.CapsulImage, + sshKeys, + ) + if err != nil { + return err + } + + tableColumns = []string{"Name", "ID"} + table = formatter.CreateTable(tableColumns) + table.Append([]string{internal.CapsulName, resp.ID}) + table.SetCaption(true, "capsul creation response") + table.Render() + + return nil } var serverNewCommand = &cli.Command{ @@ -209,12 +150,52 @@ var serverNewCommand = &cli.Command{ Aliases: []string{"n"}, Usage: "Create a new server using a 3rd party provider", Description: ` -Use a provider plugin to create a new server which can then be used to house a -new Co-op Cloud installation. +This command creates a new server via a 3rd party provider. + +The following providers are supported: + + Capsul https://git.cyberia.club/Cyberia/capsul-flask + Hetzner Cloud https://docs.hetzner.com/cloud + +You may invoke this command in "wizard" mode and be prompted for input: + + abra record new `, ArgsUsage: "", - Subcommands: []*cli.Command{ - serverNewHetznerCloudCommand, - serverNewCapsulCommand, + Flags: []cli.Flag{ + internal.ServerProviderFlag, + + // Capsul + internal.CapsulInstanceURLFlag, + internal.CapsulTypeFlag, + internal.CapsulImageFlag, + internal.CapsulSSHKeysFlag, + internal.CapsulAPITokenFlag, + + // Hetzner + internal.HetznerCloudNameFlag, + internal.HetznerCloudTypeFlag, + internal.HetznerCloudImageFlag, + internal.HetznerCloudSSHKeysFlag, + internal.HetznerCloudLocationFlag, + internal.HetznerCloudAPITokenFlag, + }, + Action: func(c *cli.Context) error { + if err := internal.EnsureServerProvider(); err != nil { + logrus.Fatal(err) + } + + switch internal.ServerProvider { + case "capsul": + if err := newCapsulVPS(c); err != nil { + logrus.Fatal(err) + } + case "hetzner-cloud": + if err := newHetznerCloudVPS(c); err != nil { + logrus.Fatal(err) + } + } + + return nil }, } diff --git a/go.mod b/go.mod index 01d18142..af9a01aa 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( ) require ( - coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d + coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e github.com/Microsoft/hcsshim v0.8.21 // indirect github.com/containerd/containerd v1.5.5 // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect diff --git a/go.sum b/go.sum index 59bfde5a..2bcce79b 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d h1:5A69AFx2BP5J43Y9SaB9LlAIMLr2SWqbzfgjUh8sgKM= -coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d/go.mod h1:HEQ9pSJRsDKabMxPfYCCzpVpAreLoC4Gh4SkVyOaKvk= +coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e h1:o5OZInc5b9esiN4hlfjZY6u0r+qB2iSv/11jnMGuR38= +coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e/go.mod h1:HEQ9pSJRsDKabMxPfYCCzpVpAreLoC4Gh4SkVyOaKvk= coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb h1:Jf+Dnna2kXcNQvcA5JMp6d2Uyvg2pIVJfip9+X5FrH0= coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=