// Copyright (c) 2019, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. package remote import ( "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "strings" "time" useragent "github.com/sylabs/singularity/pkg/util/user-agent" yaml "gopkg.in/yaml.v2" ) var ( // ErrNoDefault indicates no default remote being set ErrNoDefault = errors.New("no default remote") ) var errorCodeMap = map[int]string{ 404: "Invalid Token", 500: "Internal Server Error", } // Config stores the state of remote endpoint configurations type Config struct { DefaultRemote string `yaml:"Active"` Remotes map[string]*EndPoint `yaml:"Remotes"` } // EndPoint descriptes a single remote service type EndPoint struct { URI string `yaml:"URI,omitempty"` Token string `yaml:"Token,omitempty"` System bool `yaml:"System"` // Was this EndPoint set from system config file } // ReadFrom reads remote configuration from io.Reader // returns Config populated with remotes func ReadFrom(r io.Reader) (*Config, error) { c := &Config{ Remotes: make(map[string]*EndPoint), } // read all data from r into b b, err := ioutil.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read from io.Reader: %s", err) } if len(b) > 0 { // If we had data to read in io.Reader, attempt to unmarshal as YAML. // Also, it will fail if the YAML file does not have the expected // structure. if err := yaml.UnmarshalStrict(b, c); err != nil { return nil, fmt.Errorf("failed to decode YAML data from io.Reader: %s", err) } } return c, nil } // WriteTo writes the configuration to the io.Writer // returns and error if write is incomplete func (c *Config) WriteTo(w io.Writer) (int64, error) { yaml, err := yaml.Marshal(c) if err != nil { return 0, fmt.Errorf("failed to marshall remote config to yaml: %v", err) } n, err := w.Write(yaml) if err != nil { return 0, fmt.Errorf("failed to write remote config to io.Writer: %v", err) } return int64(n), err } // SyncFrom updates c with the remotes specified in sys. Typically, this is used // to sync a globally-configured remote.Config into a user-specific remote.Config. // Currently, SyncFrom will return a name-collision error if there is an EndPoint // name which exists in both c & sys, and the EndPoint in c has System == false. func (c *Config) SyncFrom(sys *Config) error { for name, eSys := range sys.Remotes { eUsr, err := c.GetRemote(name) if err == nil && !eUsr.System { // usr & sys name collision return fmt.Errorf("name collision while syncing: %s", name) } else if err == nil { eUsr.URI = eSys.URI // update URI just in case continue } e := &EndPoint{ URI: eSys.URI, System: true, } if err := c.Add(name, e); err != nil { return err } } // set system default to user default if no user default specified if c.DefaultRemote == "" && sys.DefaultRemote != "" { c.DefaultRemote = sys.DefaultRemote } return nil } // SetDefault sets default remote endpoint or returns an error if it does not exist func (c *Config) SetDefault(name string) error { if _, ok := c.Remotes[name]; !ok { return fmt.Errorf("%s is not a remote", name) } c.DefaultRemote = name return nil } // GetDefault returns default remote endpoint or an error func (c *Config) GetDefault() (*EndPoint, error) { if c.DefaultRemote == "" { return nil, ErrNoDefault } if _, ok := c.Remotes[c.DefaultRemote]; !ok { return nil, fmt.Errorf("%s is not a remote", c.DefaultRemote) } return c.Remotes[c.DefaultRemote], nil } // Add a new remote endpoint // returns an error if it already exists func (c *Config) Add(name string, e *EndPoint) error { if _, ok := c.Remotes[name]; ok { return fmt.Errorf("%s is already a remote", name) } c.Remotes[name] = e return nil } // Remove a remote endpoint // if endpoint is the default, the default is cleared // returns an error if it does not exist func (c *Config) Remove(name string) error { if _, ok := c.Remotes[name]; !ok { return fmt.Errorf("%s is not a remote", name) } if c.DefaultRemote == name { c.DefaultRemote = "" } delete(c.Remotes, name) return nil } // GetRemote returns a reference to an existing endpoint // returns error if remote does not exist func (c *Config) GetRemote(name string) (*EndPoint, error) { r, ok := c.Remotes[name] if !ok { return nil, fmt.Errorf("%s is not a remote", name) } return r, nil } // Rename an existing remote // returns an error if it does not exist func (c *Config) Rename(name, newName string) error { if _, ok := c.Remotes[name]; !ok { return fmt.Errorf("%s is not a remote", name) } if _, ok := c.Remotes[newName]; ok { return fmt.Errorf("%s is already a remote", newName) } if c.DefaultRemote == name { c.DefaultRemote = newName } c.Remotes[newName] = c.Remotes[name] delete(c.Remotes, name) return nil } // VerifyToken returns an error if a token is not valid func (e *EndPoint) VerifyToken() error { baseURL, err := e.GetServiceURI("token") if err != nil { return fmt.Errorf("while getting token service uri: %v", err) } client := &http.Client{ Timeout: (30 * time.Second), } req, err := http.NewRequest(http.MethodGet, baseURL+"/v1/token-status", nil) if err != nil { return err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", e.Token)) req.Header.Set("User-Agent", useragent.Value()) res, err := client.Do(req) if err != nil { return fmt.Errorf("error making request to server: %v", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { convStatus, ok := errorCodeMap[res.StatusCode] if !ok { convStatus = "Unknown" } return fmt.Errorf("error response from server: %v", convStatus) } return nil } func getCloudConfig(uri string) ([]byte, error) { client := &http.Client{ Timeout: (30 * time.Second), } url := "https://" + uri + "/assets/config/config.prod.json" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", useragent.Value()) res, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error making request to server: %v", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("error response from server: %v", res.StatusCode) } b, err := ioutil.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("while reading response body: %v", err) } return b, nil } // GetServiceURI returns the URI for the service at the specified SCS endpoint // Examples of services: consent, build, library, key, token func (e *EndPoint) GetServiceURI(service string) (string, error) { b, err := getCloudConfig(e.URI) if err != nil { return "", err } var a map[string]map[string]interface{} if err := json.Unmarshal(b, &a); err != nil { return "", fmt.Errorf("jsonresp: failed to unmarshal response: %v", err) } val, ok := a[service+"API"] if !ok { return "", fmt.Errorf("%v is not a service at endpoint", service) } uri, ok := val["uri"].(string) if !ok { return "", fmt.Errorf("%v service at endpoint failed to provide URI in response", service) } return uri, nil } // GetAllServiceURIs returns all available service urls for a given endpoint in a map func (e *EndPoint) GetAllServiceURIs() (map[string]string, error) { b, err := getCloudConfig(e.URI) if err != nil { return nil, err } var a map[string]map[string]interface{} if err := json.Unmarshal(b, &a); err != nil { return nil, fmt.Errorf("jsonresp: failed to unmarshal response: %v", err) } uris := make(map[string]string) for k := range a { if strings.HasSuffix(k, "API") { if s, ok := a[k]["uri"].(string); ok { uris[strings.TrimSuffix(k, "API")] = s } } } return uris, nil }