From 1e03b0168164dc4f348de5331cbc06aa949ca158 Mon Sep 17 00:00:00 2001 From: Chris Grindstaff Date: Fri, 28 Jul 2023 11:06:09 -0400 Subject: [PATCH] ci: include Harvest certification tool Fixes: #1914 --- integration/certer/main.go | 389 ++++++++++++++++++++++++++++ integration/certer/models/models.go | 159 ++++++++++++ integration/go.mod | 8 +- integration/go.sum | 19 +- 4 files changed, 565 insertions(+), 10 deletions(-) create mode 100644 integration/certer/main.go create mode 100644 integration/certer/models/models.go diff --git a/integration/certer/main.go b/integration/certer/main.go new file mode 100644 index 000000000..673d1c99b --- /dev/null +++ b/integration/certer/main.go @@ -0,0 +1,389 @@ +package main + +import ( + "context" + "crypto/tls" + "errors" + "flag" + "fmt" + "github.com/Netapp/harvest-automation/certer/models" + "github.com/Netapp/harvest-automation/test/utils" + "github.com/carlmjohnson/requests" + "github.com/rs/zerolog/log" + "net/http" + "os" + "os/exec" + "time" +) + +const ( + commonName = "Harvest2" + harvestUser = "harvest" + ontapRole = "harvest" + ontapRestRole = "harvest-rest" +) + +var ( + username = "admin" + password string + ip string + adminSVM string + clientKeyDir = "/opt/harvest/cert" + clientKeyName = "u2" + force bool +) + +func main() { + utils.SetupLogging() + parseCLI() + begin() +} + +func parseCLI() { + flag.StringVar(&ip, "ip", "", "IP of ONTAP cluster (required)") + flag.StringVar(&username, "username", "", "Username of ONTAP admin user (default=admin)") + flag.StringVar(&password, "password", "", "Password of ONTAP admin user (required)") + flag.StringVar(&clientKeyDir, "keydir", ".", "Directory to write cert files to") + flag.StringVar(&clientKeyName, "keyname", "u2", "Prefix name to use for cert files") + flag.BoolVar(&force, "force", false, "Always create certs even if the current ones are still valid") + + flag.Parse() + if ip == "" { + printRequired("ip") + } + if password == "" { + printRequired("password") + } +} + +func printRequired(name string) { + fmt.Printf("%s address is required\n", name) + fmt.Printf("usage: \n") + flag.PrintDefaults() + os.Exit(1) +} + +func begin() { + log.Info().Str("ip", ip).Msg("Create certificates for ip") + + // Get admin SVM + fetchAdminSVM() + + // Query for existing CA + certificates, err := fetchCA() + if err != nil { + log.Error().Err(err).Msg("") + return + } + + // Check if these certs have expired + if !force && certsAreFresh(certificates) { + return + } + + // Create private key and certificate signing request (CSR) + csr, err := ensureOpenSSLInstalled() + if err != nil { + log.Error().Err(err).Msg("") + return + } + + // Delete existing + if certificates.NumRecords > 0 { + log.Info(). + Int("num", certificates.NumRecords). + Str("common_name", commonName). + Msg("Deleting matching certificates") + err := deleteCertificates(certificates) + if err != nil { + log.Error().Err(err).Msg("failed to delete certificates") + return + } + } + + // Create a root CA certificate that will be used to sign certificate requests for the user account(s) + err = createRootCA() + if err != nil { + log.Error().Err(err).Msg("failed") + return + } + + // Sign the locally created certificate with the root CA generated above + err = signCSR(csr) + if err != nil { + log.Error().Err(err).Msg("failed") + return + } + + // Add certificate auth to this ONTAP user + err = addCertificateAuthToHarvestUser() + if err != nil { + log.Error().Err(err).Msg("") + } + + fmt.Printf("Success! Test with:\n") + fmt.Printf("curl --insecure --cert %s --key %s \"https://%s/api/cluster?fields=version\"\n", + local(".crt"), local(".key"), ip) + curlServer() +} + +func sleep(s string) { + duration, err := time.ParseDuration(s) + if err != nil { + log.Error().Err(err).Msg("failed to sleep") + } + log.Info().Str("sleep", s).Msg("sleep") + time.Sleep(duration) +} + +func curlServer() { + if _, err := os.Stat(local(".crt")); errors.Is(err, os.ErrNotExist) { + log.Panic().Str("crt", local(".crt")).Msg("does not exist") + } + for i := 0; i < 60; i++ { + //nolint:gosec + command := exec.Command("curl", "--insecure", "--cert", local(".crt"), "--key", local(".key"), + fmt.Sprintf("https://%s/api/cluster?fields=version", ip)) + output, err := command.CombinedOutput() + if err != nil { + log.Error().Err(err).Str("output", string(output)).Msg("failed to exec curl") + } else { + fmt.Println(string(output)) + return + } + sleep("1s") + } +} + +func certsAreFresh(certificates models.Certificates) bool { + cert := certificates.Records[0] + date := cert.ExpiryTime.Format("2006-01-02") + log.Info().Str("expire", date).Msg("Certificates are fresh. Done") + return cert.ExpiryTime.After(time.Now().Add(8 * time.Hour)) +} + +func addCertificateAuthToHarvestUser() error { + perms := []models.SecurityPermissions{ + { + Application: "http", + Role: ontapRestRole, + AuthMethod: "cert", + User: harvestUser, + }, + { + Application: "ontapi", + Role: ontapRole, + AuthMethod: "cert", + User: harvestUser, + }, + } + for _, perm := range perms { + p := perm + err := newRequest(). + Pathf("/api/private/cli/security/login"). + BodyJSON(&p). + Fetch(context.Background()) + + if err != nil { + var oe models.OntapError + if errors.As(err, &oe) { + if oe.StatusCode == 409 { + // duplicate entry - that's fine, ignore + continue + } + } + return fmt.Errorf("failed to add cert auth to user=%s err=%w", harvestUser, err) + } + } + return nil +} + +func fetchCA() (models.Certificates, error) { + var certificates models.Certificates + err := newRequest(). + Pathf("/api/security/certificates"). + Param("common_name", commonName). + Param("fields", "**"). + ToJSON(&certificates). + Fetch(context.Background()) + if err != nil { + return models.Certificates{}, err + } + return certificates, nil +} + +func signCSR(csr string) error { + certificates, err := fetchCA() + if err != nil { + return fmt.Errorf("failed to fetch CA err=%w", err) + } + + ca := findRootCA(certificates) + if ca == nil { + return fmt.Errorf("unable to find CA") + } + // This is needed because you can't create a signing request with an expiry longer than the CA's expiry. + // Use one day less than the number of days until the CA expires + days := int(time.Until(ca.ExpiryTime).Hours()/24) - 1 + var signResponse models.SignResponse + expiry := fmt.Sprintf("P%dDT", days) + newCa := models.NewSignRequest{ + ExpiryTime: expiry, + SigningRequest: csr, + HashFunction: "sha256", + } + + err = newRequest(). + Pathf("/api/security/certificates/%s/sign", ca.UUID). + BodyJSON(&newCa). + ToJSON(&signResponse). + Fetch(context.Background()) + if err != nil { + return fmt.Errorf("failed to create signed cert err=%w", err) + } + + localCert := local(".crt") + err = os.WriteFile(localCert, []byte(signResponse.PublicCertificate), 0600) + if err != nil { + return fmt.Errorf("failed to write %s err=%w", localCert, err) + } + return nil +} + +func findRootCA(certificates models.Certificates) *models.Cert { + for _, record := range certificates.Records { + if record.Type == "root_ca" { + return &record + } + } + return nil +} + +func ensureOpenSSLInstalled() (string, error) { + _, err := exec.LookPath("openssl") + if err != nil { + return "", err + } + privateKey := local(".key") + csr := local(".csr") + command := exec.Command("openssl", "genrsa", "-out", privateKey, "2048") + output, err := command.CombinedOutput() + if err != nil { + return "", fmt.Errorf("err=%w output=%s", err, output) + } + log.Debug().Str("output", string(output)).Msg("created private key") + // openssl req -days 3650 -sha256 -new -nodes -key cert/u2.key -subj /CN=harvest -out u2.csr + + command = exec.Command("openssl", "req", "-days", "3650", "-sha256", "-new", "-nodes", "-key", privateKey, + "-subj", "/CN="+harvestUser, "-out", csr) + output, err = command.CombinedOutput() + if err != nil { + return "", fmt.Errorf("error creating csr err=%w output=%s", err, output) + } + + log.Debug().Str("output", string(output)).Msg("created csr") + log.Info().Str("privateKey", privateKey).Msg("Created private key and certificate signing request (CSR)") + + data, err := os.ReadFile(csr) + if err != nil { + return "", fmt.Errorf("failed to read csr file=%s err=%w", csr, err) + } + + return string(data), nil +} + +func createRootCA() error { + // 10 year expiry + tenYears := fmt.Sprintf("P%dDT", 365*10) + newCa := models.NewCA{ + Svm: models.SVM{Name: adminSVM}, + Type: "root-ca", + CommonName: commonName, + ExpiryTime: tenYears, + } + err := newRequest(). + Pathf("/api/security/certificates"). + BodyJSON(&newCa). + Fetch(context.Background()) + if err != nil { + return fmt.Errorf("failed to create root CA err=%w", err) + } + log.Info().Msg("Created Root CA") + return nil +} + +func fetchAdminSVM() { + var svmResp models.SVMResponse + err := newRequest(). + Pathf("/api/private/cli/vserver"). + Param("type", "admin"). + Param("fields", "type,uuid"). + ToJSON(&svmResp). + Fetch(context.Background()) + if err != nil { + log.Error().Err(err).Msg("failed to fetch admin SVM") + return + } + adminSVM = svmResp.Records[0].Vserver +} + +func newRequest() *requests.Builder { + return requests. + URL(fmt.Sprintf("https://%s", ip)). + BasicAuth(username, password). + AddValidator(func(response *http.Response) error { + if response.StatusCode >= 400 { + var ontapErr models.OntapError + //nolint:bodyclose + err := requests.ToJSON(&ontapErr)(response) + if err != nil { + return err + } + ontapErr.StatusCode = response.StatusCode + return ontapErr + } + return nil + }). + Client(newClient()) +} + +func deleteCertificates(certificates models.Certificates) error { + // Three certificates are returned: server_ca, client_ca, root_ca + for _, record := range certificates.Records { + var resp string + err := newRequest(). + Pathf("/api/security/certificates/%s", record.UUID). + ToString(&resp). + AddValidator(func(response *http.Response) error { + if response.StatusCode != 200 { + return fmt.Errorf("failed to delete ertificates. statusCode=%d status=%s", response.StatusCode, response.Status) + } + return nil + }). + Delete(). + Fetch(context.Background()) + + if err != nil { + return err + } + } + return nil +} + +func newClient() *http.Client { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec + }, + } + client := &http.Client{ + Transport: transport, + Timeout: 2 * time.Minute, + } + + return client +} + +func local(ext string) string { + return fmt.Sprintf("%s/%s%s", clientKeyDir, clientKeyName, ext) +} diff --git a/integration/certer/models/models.go b/integration/certer/models/models.go new file mode 100644 index 000000000..745ddcada --- /dev/null +++ b/integration/certer/models/models.go @@ -0,0 +1,159 @@ +package models + +import ( + "fmt" + "time" +) + +type Cert struct { + UUID string `json:"uuid"` + Scope string `json:"scope"` + Type string `json:"type"` + CommonName string `json:"common_name"` + SerialNumber string `json:"serial_number"` + Ca string `json:"ca"` + HashFunction string `json:"hash_function"` + KeySize int `json:"key_size"` + ExpiryTime time.Time `json:"expiry_time"` + PublicCertificate string `json:"public_certificate"` + Name string `json:"name"` + AuthorityKeyIdentifier string `json:"authority_key_identifier"` + SubjectKeyIdentifier string `json:"subject_key_identifier"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + } `json:"_links"` +} + +type Certificates struct { + Records []Cert `json:"records"` + NumRecords int `json:"num_records"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + } `json:"_links"` +} + +type SVMResponse struct { + Records []struct { + Vserver string `json:"vserver"` + UUID string `json:"uuid"` + Type string `json:"type"` + } `json:"records"` + NumRecords int `json:"num_records"` +} + +type SVM struct { + Name string `json:"name,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +type NewCA struct { + PrivateKey string `json:"private_key,omitempty"` + IntermediateCertificates []string `json:"intermediate_certificates,omitempty"` + ExpiryTime string `json:"expiry_time,omitempty"` + CommonName string `json:"common_name,omitempty"` + KeySize int `json:"key_size,omitempty"` + HashFunction string `json:"hash_function,omitempty"` + Name string `json:"name,omitempty"` + PublicCertificate string `json:"public_certificate,omitempty"` + Type string `json:"type,omitempty"` + Svm SVM `json:"svm,omitempty"` +} + +type RootCA struct { + SerialNumber string `json:"serial_number"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + } `json:"_links"` + PrivateKey string `json:"private_key"` + UUID string `json:"uuid"` + IntermediateCertificates []string `json:"intermediate_certificates"` + ExpiryTime string `json:"expiry_time"` + CommonName string `json:"common_name"` + Scope string `json:"scope"` + AuthorityKeyIdentifier string `json:"authority_key_identifier"` + KeySize int `json:"key_size"` + HashFunction string `json:"hash_function"` + Name string `json:"name"` + Ca string `json:"ca"` + SubjectKeyIdentifier string `json:"subject_key_identifier"` + PublicCertificate string `json:"public_certificate"` + Type string `json:"type"` + Svm struct { + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + } `json:"_links"` + Name string `json:"name"` + UUID string `json:"uuid"` + } `json:"svm"` +} + +type CreateRootCAResponse struct { + Records []RootCA `json:"records"` + Links struct { + Next struct { + Href string `json:"href"` + } `json:"next"` + Self struct { + Href string `json:"href"` + } `json:"self"` + } `json:"_links"` + NumRecords int `json:"num_records"` +} + +type OErr struct { + Message string `json:"message"` + Code string `json:"code"` + Target string `json:"target"` +} + +type OntapError struct { + Err OErr `json:"error"` + StatusCode int +} + +func (o OntapError) Error() string { + return fmt.Sprintf("message: %s code: %s", o.Err.Message, o.Err.Code) +} + +type NewSignRequest struct { + HashFunction string `json:"hash_function,omitempty"` + ExpiryTime string `json:"expiry_time,omitempty"` + SigningRequest string `json:"signing_request,omitempty"` +} + +type SignResponse struct { + PublicCertificate string `json:"public_certificate,omitempty"` +} + +type Apps struct { + AuthenticationMethods []string `json:"authentication_methods,omitempty"` + Application string `json:"application,omitempty"` + SecondAuthenticationMethod string `json:"second_authentication_method,omitempty"` +} + +type Role struct { + Name string `json:"name,omitempty"` +} + +type PatchUser struct { + Password string `json:"password,omitempty"` + Locked bool `json:"locked,omitempty"` + Comment string `json:"comment,omitempty"` + Applications []Apps `json:"applications,omitempty"` + Role Role `json:"role,omitempty"` +} + +type SecurityPermissions struct { + Application string `json:"application,omitempty"` + Role string `json:"role,omitempty"` + AuthMethod string `json:"authentication_method,omitempty"` + User string `json:"user_or_group_name,omitempty"` +} diff --git a/integration/go.mod b/integration/go.mod index 16a2459e0..e8321114e 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -5,10 +5,11 @@ go 1.20 replace github.com/netapp/harvest/v2 => ../ require ( + github.com/carlmjohnson/requests v0.23.4 github.com/netapp/harvest/v2 v2.0.0-20230404163343-f21b0c1a08ac github.com/rs/zerolog v1.29.1 github.com/tidwall/gjson v1.14.4 - golang.org/x/text v0.10.0 + golang.org/x/text v0.11.0 ) require ( @@ -20,7 +21,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect - github.com/shirou/gopsutil/v3 v3.23.5 // indirect + github.com/shirou/gopsutil/v3 v3.23.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -29,6 +30,7 @@ require ( github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect - golang.org/x/sys v0.9.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.10.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/integration/go.sum b/integration/go.sum index 662c115ef..1b4dc8607 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/carlmjohnson/requests v0.23.4 h1:AxcvapfB9RPXLSyvAHk9YJoodQ43ZjzNHj6Ft3tQGdg= +github.com/carlmjohnson/requests v0.23.4/go.mod h1:Qzp6tW4DQyainPP+tGwiJTzwxvElTIKm0B191TgTtOA= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,8 +37,8 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v3 v3.23.5 h1:5SgDCeQ0KW0S4N0znjeM/eFHXXOKyv2dVNgRq/c9P6Y= -github.com/shirou/gopsutil/v3 v3.23.5/go.mod h1:Ng3Maa27Q2KARVJ0SPZF5NdrQSC3XHKP8IIWrHgMeLY= +github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHEHT08= +github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -50,8 +52,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS 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.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -66,6 +68,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -74,10 +78,11 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=