Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multipart mime support and refactor #21

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package config

import (
"bufio"
"fmt"
"net/textproto"
"reflect"
"regexp"
"strings"
Expand Down Expand Up @@ -56,6 +58,18 @@ func IsCloudConfig(userdata string) bool {
return (header == "#cloud-config")
}

func IsMultipartMime(userdata string) bool {
bufioReader := bufio.NewReader(strings.NewReader(userdata))
textProtoReader := textproto.NewReader(bufioReader)
header, err := textProtoReader.ReadMIMEHeader()
if err != nil {
return false
}

contentType := header.Get("Content-Type")
return strings.Contains(contentType, "multipart/mixed")
krnowak marked this conversation as resolved.
Show resolved Hide resolved
}

// NewCloudConfig instantiates a new CloudConfig from the given contents (a
// string of YAML), returning any error encountered. It will ignore unknown
// fields but log encountering them.
Expand Down
4 changes: 3 additions & 1 deletion config/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ func Validate(userdataBytes []byte) (Report, error) {
return Report{}, nil
case config.IsCloudConfig(string(userdataBytes)):
return validateCloudConfig(userdataBytes, Rules)
case config.IsMultipartMime(string(userdataBytes)):
return Report{}, nil
default:
return Report{entries: []Entry{
{kind: entryError, message: `must be "#cloud-config" or begin with "#!"`, line: 1},
{kind: entryError, message: `must be "#cloud-config", multipart mime or begin with "#!"`, line: 1},
}}, nil
}
}
Expand Down
2 changes: 1 addition & 1 deletion config/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func TestValidate(t *testing.T) {
},
{
config: "{}",
report: Report{entries: []Entry{{entryError, `must be "#cloud-config" or begin with "#!"`, 1}}},
report: Report{entries: []Entry{{entryError, `must be "#cloud-config", multipart mime or begin with "#!"`, 1}}},
},
{
config: `{"ignitionVersion":0}`,
Expand Down
153 changes: 87 additions & 66 deletions coreos-cloudinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func main() {
os.Exit(2)
}

if flags.printVersion == true {
if flags.printVersion {
fmt.Printf("coreos-cloudinit %s\n", version)
os.Exit(0)
}
Expand All @@ -188,6 +188,22 @@ func main() {
os.Exit(1)
}

log.Printf("Fetching meta-data from datasource of type %q\n", ds.Type())
metadata, err := ds.FetchMetadata()
if err != nil {
log.Printf("Failed fetching meta-data from datasource: %v\n", err)
os.Exit(1)
}
env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.sshKeyName, metadata)

// Setup networking units
if flags.convertNetconf != "" {
if err := setupNetworkUnits(metadata.NetworkConfig, env, flags.convertNetconf); err != nil {
log.Printf("Failed to setup network units: %v\n", err)
os.Exit(1)
}
}

log.Printf("Fetching user-data from datasource of type %q\n", ds.Type())
userdataBytes, err := ds.FetchUserdata()
if err != nil {
Expand Down Expand Up @@ -216,66 +232,40 @@ func main() {
}
}

log.Printf("Fetching meta-data from datasource of type %q\n", ds.Type())
metadata, err := ds.FetchMetadata()
udata, err := initialize.NewUserData(string(userdataBytes), env)
if err != nil {
log.Printf("Failed fetching meta-data from datasource: %v\n", err)
os.Exit(1)
}

// Apply environment to user-data
env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.sshKeyName, metadata)
userdata := env.Apply(string(userdataBytes))

var ccu *config.CloudConfig
var script *config.Script
switch ud, err := initialize.ParseUserData(userdata); err {
case initialize.ErrIgnitionConfig:
fmt.Printf("Detected an Ignition config. Exiting...")
os.Exit(0)
case nil:
switch t := ud.(type) {
case *config.CloudConfig:
ccu = t
case *config.Script:
script = t
}
default:
fmt.Printf("Failed to parse user-data: %v\nContinuing...\n", err)
log.Printf("Failed to parse user-data: %v\nContinuing...\n", err)
failure = true
}

log.Println("Merging cloud-config from meta-data and user-data")
cc := mergeConfigs(ccu, metadata)
mustStop := false
hostname := determineHostname(metadata, udata)
if err := initialize.ApplyHostname(hostname); err != nil {
log.Printf("Failed to set hostname: %v", err)
mustStop = true
}
Comment on lines +242 to +246
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now overwrites any static hostname with the meta-data hostname. Can we remove these lines again?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was kind of the point of this. If coreos-cloudinit is used, it is responsible for setting the hostname. The hostname is fetched from userdata or meta-data and the short form hostname is set. Userdata has precedence.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In particular, when Ignition ran and coreos-cloudinit gets triggered through the config drive mechanism, this suddenly overwrites the hostname Ignition set up in /etc/hostname.
We could try to skip execution of coreos-cloudinit when Ignition ran but still I wonder if this here wouldn't also cause problems when coreos-cloudinit or some custom image setup was writing to /etc/hostname and now this here overwrites it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we should use coreos-cloudinit in scenarios where it's needed (like OpenStack or wherever we need multipart mime/cloud-config). Otherwise it should be disabled.

If, however it's enabled, we should assume it will overwrite some things set by afterburn.

If custom image setup is needed, that should either be done with coreos-cloudinit via userdata, or it should run after coreos-cloudinit.

Otherwise there is no sane way to have coreos-cloudinit run and be useful. Perhaps this is something that needs to be documented?

Copy link
Member

@pothos pothos Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously the process terminated with Detected an Ignition config. Exiting... but that's maybe something we better enforce from the service unit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it should probably not run if the userdata is ignition. Is there some other trigger that enables it, besides non-ignition userdata?

If yes, we should probably add flags to disable various bits of it, like setting the hostname and SSH keys (for example).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think flags are a good idea, we also have this for afterburn and it would allow to, e.g., opt out of metadata if it's covered by afterburn for the platform. We should also tweak the units to not have it run at all on certain platforms: e.g., on Digital Ocean it can run twice, once through the regular oem-cloudinit service and once through the configdrive, and it makes sense to disable it for the config drive (this is the case we ran into).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look into adding those flags as soon as I can.


var ifaces []network.InterfaceGenerator
if flags.convertNetconf != "" {
var err error
switch flags.convertNetconf {
case "debian":
ifaces, err = network.ProcessDebianNetconf(metadata.NetworkConfig.([]byte))
case "packet":
ifaces, err = network.ProcessPacketNetconf(metadata.NetworkConfig.(packet.NetworkData))
case "vmware":
ifaces, err = network.ProcessVMwareNetconf(metadata.NetworkConfig.(map[string]string))
default:
err = fmt.Errorf("Unsupported network config format %q", flags.convertNetconf)
}
if err != nil {
log.Printf("Failed to generate interfaces: %v\n", err)
os.Exit(1)
}
mergedKeys := mergeSSHKeysFromSources(metadata, udata)
if err := initialize.ApplyCoreUserSSHKeys(mergedKeys, env); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also a new addition to the code path? I noticed that this conflicts with afterburn writing the keys as well, and this race could maybe lead to a broken setup.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afterburn runs in initrd. This runs during system service startup. At most, I think this can add duplicate keys, but that should not break anything. This is not new, just moved around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[email protected] is running at the same time but in this execution log here it was faster than cloudinit:

1377   │ Jun 20 08:34:24.519522 coreos-cloudinit[1171]: 2023/06/20 08:34:24 Checking availability of "cloud-drive"
1378   │ Jun 20 08:34:24.519522 coreos-cloudinit[1171]: 2023/06/20 08:34:24 Fetching meta-data from datasource of type "cloud-drive"
1379   │ Jun 20 08:34:24.519522 coreos-cloudinit[1171]: 2023/06/20 08:34:24 Attempting to read from "/media/configdrive/openstack/latest/meta_data.json"
1380   │ Jun 20 08:34:24.519522 coreos-cloudinit[1171]: 2023/06/20 08:34:24 Fetching user-data from datasource of type "cloud-drive"
1381   │ Jun 20 08:34:24.519522 coreos-cloudinit[1171]: 2023/06/20 08:34:24 Attempting to read from "/media/configdrive/openstack/latest/user_data"
1382   │ Jun 20 08:34:24.524397 update-ssh-keys[1183]: Updated "/home/core/.ssh/authorized_keys"
1383   │ Jun 20 08:34:24.521093 systemd[1]: Finished [email protected] - Flatcar Metadata Agent (SSH Keys).

log.Printf("Failed to apply SSH keys: %v", err)
mustStop = true
}

if err = initialize.Apply(cc, ifaces, env); err != nil {
log.Printf("Failed to apply cloud-config: %v\n", err)
if mustStop {
// We try to set both the hostname and SSH keys. If either fails, we stop.
// We don't stop if hostname fails to be set, because we may still be able to set
// the SSH keys and access the server to debug. However, if an error is encountered
// in either of the two operations, we exit with a non-zero status.
os.Exit(1)
}

if script != nil {
if err = runScript(*script, env); err != nil {
log.Printf("Failed to run script: %v\n", err)
os.Exit(1)
if !failure && udata != nil {
for _, part := range udata.Parts {
log.Printf("Running part %q (%s)", part.PartName(), part.PartType())
if err := part.RunPart(env); err != nil {
log.Printf("Failed to run part %q: %v", part.PartName(), err)
failure = true
}
}
}

Expand All @@ -284,25 +274,56 @@ func main() {
}
}

// mergeConfigs merges certain options from md (meta-data from the datasource)
// onto cc (a CloudConfig derived from user-data), if they are not already set
// on cc (i.e. user-data always takes precedence)
func mergeConfigs(cc *config.CloudConfig, md datasource.Metadata) (out config.CloudConfig) {
if cc != nil {
out = *cc
}

if md.Hostname != "" {
if out.Hostname != "" {
log.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", out.Hostname, md.Hostname)
} else {
out.Hostname = md.Hostname
// determineHostname returns either the hostname from the metadata, or the hostname from the
// supplied cloud-config. The cloud-config hostname takes precedence, and we stop after the first
// cloud-config that gives us a hostname.
func determineHostname(md datasource.Metadata, udata *initialize.UserData) string {
hostname := md.Hostname
if udata != nil {
udataHostname := udata.FindHostname()
if udataHostname != "" {
hostname = udataHostname
}
}
return hostname
}

// mergeSSHKeysFromSources creates a list of all SSH keys from meta-data and the supplied
// cloud-config sources.
func mergeSSHKeysFromSources(md datasource.Metadata, udata *initialize.UserData) []string {
keys := []string{}
for _, key := range md.SSHPublicKeys {
out.SSHAuthorizedKeys = append(out.SSHAuthorizedKeys, key)
keys = append(keys, key)
}

if udata != nil {
return udata.FindSSHKeys(keys)
}

return keys
}

func setupNetworkUnits(netConfig interface{}, env *initialize.Environment, netconf string) error {
var ifaces []network.InterfaceGenerator
var err error
switch netconf {
case "debian":
ifaces, err = network.ProcessDebianNetconf(netConfig.([]byte))
case "packet":
ifaces, err = network.ProcessPacketNetconf(netConfig.(packet.NetworkData))
case "vmware":
ifaces, err = network.ProcessVMwareNetconf(netConfig.(map[string]string))
default:
err = fmt.Errorf("Unsupported network config format %q", netconf)
}
if err != nil {
return fmt.Errorf("error generating interfaces: %w", err)
}

if err := initialize.ApplyNetworkConfig(ifaces, env); err != nil {
return fmt.Errorf("error applying network config: %w", err)
}
return
return nil
}

// getDatasources creates a slice of possible Datasources for cloudinit based
Expand Down
70 changes: 0 additions & 70 deletions coreos-cloudinit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,79 +18,9 @@ import (
"bytes"
"encoding/base64"
"errors"
"reflect"
"testing"

"github.com/flatcar/coreos-cloudinit/config"
"github.com/flatcar/coreos-cloudinit/datasource"
)

func TestMergeConfigs(t *testing.T) {
tests := []struct {
cc *config.CloudConfig
md datasource.Metadata

out config.CloudConfig
}{
{
// If md is empty and cc is nil, result should be empty
out: config.CloudConfig{},
},
{
// If md and cc are empty, result should be empty
cc: &config.CloudConfig{},
out: config.CloudConfig{},
},
{
// If cc is empty, cc should be returned unchanged
cc: &config.CloudConfig{SSHAuthorizedKeys: []string{"abc", "def"}, Hostname: "cc-host"},
out: config.CloudConfig{SSHAuthorizedKeys: []string{"abc", "def"}, Hostname: "cc-host"},
},
{
// If cc is empty, cc should be returned unchanged(overridden)
cc: &config.CloudConfig{},
md: datasource.Metadata{Hostname: "md-host", SSHPublicKeys: map[string]string{"key": "ghi"}},
out: config.CloudConfig{SSHAuthorizedKeys: []string{"ghi"}, Hostname: "md-host"},
},
{
// If cc is nil, cc should be returned unchanged(overridden)
md: datasource.Metadata{Hostname: "md-host", SSHPublicKeys: map[string]string{"key": "ghi"}},
out: config.CloudConfig{SSHAuthorizedKeys: []string{"ghi"}, Hostname: "md-host"},
},
{
// user-data should override completely in the case of conflicts
cc: &config.CloudConfig{SSHAuthorizedKeys: []string{"abc", "def"}, Hostname: "cc-host"},
md: datasource.Metadata{Hostname: "md-host"},
out: config.CloudConfig{SSHAuthorizedKeys: []string{"abc", "def"}, Hostname: "cc-host"},
},
{
// Mixed merge should succeed
cc: &config.CloudConfig{SSHAuthorizedKeys: []string{"abc", "def"}, Hostname: "cc-host"},
md: datasource.Metadata{Hostname: "md-host", SSHPublicKeys: map[string]string{"key": "ghi"}},
out: config.CloudConfig{SSHAuthorizedKeys: []string{"abc", "def", "ghi"}, Hostname: "cc-host"},
},
{
// Completely non-conflicting merge should be fine
cc: &config.CloudConfig{Hostname: "cc-host"},
md: datasource.Metadata{SSHPublicKeys: map[string]string{"zaphod": "beeblebrox"}},
out: config.CloudConfig{Hostname: "cc-host", SSHAuthorizedKeys: []string{"beeblebrox"}},
},
{
// Non-mergeable settings in user-data should not be affected
cc: &config.CloudConfig{Hostname: "cc-host", ManageEtcHosts: config.EtcHosts("lolz")},
md: datasource.Metadata{Hostname: "md-host"},
out: config.CloudConfig{Hostname: "cc-host", ManageEtcHosts: config.EtcHosts("lolz")},
},
}

for i, tt := range tests {
out := mergeConfigs(tt.cc, tt.md)
if !reflect.DeepEqual(tt.out, out) {
t.Errorf("bad config (%d): want %#v, got %#v", i, tt.out, out)
}
}
}

func mustDecode(in string) []byte {
out, err := base64.StdEncoding.DecodeString(in)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ require (
github.com/coreos/yaml v0.0.0-20141224210557-6b16a5714269
github.com/dotcloud/docker v0.11.2-0.20140522020950-55d41c3e21e1
github.com/sigma/vmw-ovflib v0.0.0-20150531125353-56b4f44581ca
github.com/stretchr/testify v1.8.2
github.com/vmware/vmw-guestinfo v0.0.0-20170622145319-ab8497750719
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/guelfey/go.dbus v0.0.0-20131113121618-f6a3a2366cc3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
github.com/tarm/goserial v0.0.0-20140420040555-cdabc8d44e8e // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/vmware/vmw-guestinfo => github.com/sigma/vmw-guestinfo v0.0.0-20170622145319-ab8497750719
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ github.com/coreos/go-systemd v0.0.0-20140326023052-4fbc5060a317 h1:OJi3CY9dHDNVE
github.com/coreos/go-systemd v0.0.0-20140326023052-4fbc5060a317/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/yaml v0.0.0-20141224210557-6b16a5714269 h1:/1sjrpK5Mb6IwyFOKd+u7321tXfNAsj0Ci8CivZmSlo=
github.com/coreos/yaml v0.0.0-20141224210557-6b16a5714269/go.mod h1:Bl1D/T9QJhVdu6eFoLrGxN90+admDLGaLz2HXH/VzDc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dotcloud/docker v0.11.2-0.20140522020950-55d41c3e21e1 h1:YgK/YfnYMOO3tJL9N0lYOd7IG2dhs9+kPUDXqPZPQ9c=
github.com/dotcloud/docker v0.11.2-0.20140522020950-55d41c3e21e1/go.mod h1:ILmsztPoNJzhFWqgN1w/CccGRGm7j5uaU6kYvDN9scw=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
Expand All @@ -17,6 +20,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sigma/vmw-guestinfo v0.0.0-20170622145319-ab8497750719 h1:RdpTT9XV4QBLYPeqHUjGAhydFTimEjgvLDFyBfLAelI=
github.com/sigma/vmw-guestinfo v0.0.0-20170622145319-ab8497750719/go.mod h1:JrRFFC0veyh0cibh0DAhriSY7/gV3kDdNaVUOmfx01U=
github.com/sigma/vmw-ovflib v0.0.0-20150531125353-56b4f44581ca h1:zVbnn0fCxftRipg4oHS6mZIP6hcm6axhUff5jV1qcC8=
Expand All @@ -25,12 +30,23 @@ github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tarm/goserial v0.0.0-20140420040555-cdabc8d44e8e h1:DFPBoKQ4NuBYyj8GVNALoQbQqESZTJ8azlYNuvrAFTA=
github.com/tarm/goserial v0.0.0-20140420040555-cdabc8d44e8e/go.mod h1:jcMo2Odv5FpDA6rp8bnczbUolcICW6t54K3s9gOlgII=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading