Skip to content

Commit

Permalink
Merge pull request #424 from istalker2/image-transports
Browse files Browse the repository at this point in the history
Ability to configure http(s) transport for the image endpoints
  • Loading branch information
ivan4th authored Oct 2, 2017
2 parents 8a70445 + d554dea commit 1a0f691
Show file tree
Hide file tree
Showing 7 changed files with 929 additions and 36 deletions.
83 changes: 83 additions & 0 deletions docs/image-name-translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,86 @@ never had Virtlet running.
There can be any number of `VirtletImageMapping` resource. However, currently all such mappings must be in the `kube-system` namespace.
`VirtletImageMapping` resource have a precedence over file-based configs for ambiguous image names. Thus it is convenient to put
defaults into static config files and then override them with `VirtletImageMapping` resources when needed.

## Configure HTTP transport for image download

By default, the image downloader uses default transport settings: system-wide CA certificates for HTTPS URLs,
up to 9 redirects and proxy from the `HTTP_PROXY`/`HTTPS_PROXY` environment variables. However, with image translation
configs it is possible to override these default and provide custom transport configuration.

Transport settings are grouped into profiles, each with the name and bunch of configuration settings. Each translation
rule may optionally have `transport` attribute set to profile name to be used for the image URL of that rule.
Below is an example of translation config that has all possible transport settings though all of them are optional:

```yaml
translations:
- name: mySmallImage
url: https://my.host.loc/small.qcow2
transport: my-server
- name: myImage
url: https://my.host.loc/big.qcow2
transport: my-server
transports:
my-server:
timeout: 30000 # in ms. 0 = no timeout (default)
maxRedirects: 1 # at most 1 redirect allowed (i.e. 2 HTTP requests). null or missing value = any number of redirects
proxy: http://my-proxy.loc:8080
tls: # optional TLS settings. Use default system settings when not specified
certificates: # there can be any mumber of certificates. Both CA and client certificates are put here
- cert: |
-----BEGIN CERTIFICATE-----
# CA PEM block goes here
# CA certificates are recognized by IsCA:TRUE flag in the certificate. Private key is not needed in this case
# CA certificates are appended to the Linux system-wide list
-----END CERTIFICATE-----
- cert: |
-----BEGIN CERTIFICATE-----
# Client-based authentication certificate PEM block goes here
# There can be several certificates put together if they share a single key
-----END CERTIFICATE-----
key: |
-----BEGIN RSA PRIVATE KEY-----
# PEM-encoded private key
# for certificate-based client authentication private key must be present
# Also the key is not required if it already contained in the cert PEM
-----END RSA PRIVATE KEY-----
serverName: my.host.com # because the certificate is for .com but we're connecting to .loc
insecure: false # when true, no server certificate validation is going to be performed
```

When no transport profile is specified for translation rule, the default system settings are used. However,
since the default value for `transport` attribute is an empty string, defining profile with empty name can
be used to override this default for all images in that particular config:

```yaml
translations:
- name: mySmallImage
url: https://my.host.loc/small.qcow2
- name: myImage
url: https://my.host.loc/big.qcow2
transports:
"":
proxy: http://my-proxy.loc:8080 # proxy for all images without explicit transport name
```

Of course, the same settings can be put into `VirtletImageMapping` objects:

```yaml
apiVersion: "virtlet.k8s/v1"
kind: VirtletImageMapping
metadata:
name: primary
namespace: kube-system
spec:
translations:
- name: mySmallImage
url: https://my.host.loc/small.qcow2
- name: myImage
url: https://my.host.loc/big.qcow2
transports:
"":
proxy: http://my-proxy.loc:8080 # proxy for all images without explicit transport name
```
51 changes: 45 additions & 6 deletions pkg/imagetranslation/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,70 @@ import "github.com/Mirantis/virtlet/pkg/utils"

// TranslationRule represents a single translation rule from either name or regexp to Endpoint
type TranslationRule struct {

// Name defines a mapping from a fixed name
Name string `yaml:"name,omitempty" json:"name,omitempty"`

// Regex defines a mapping from all names that match this regexp. In this case replacements can be used for Endpoint.Url
Regex string `yaml:"regexp,omitempty" json:"regexp,omitempty"`

// Endpoint that this rule maps to
utils.Endpoint `yaml:",inline" json:",inline"`
// Url is the image URL
Url string `yaml:"url,omitempty" json:"url,omitempty"`

// Transport is the optional transport profile name to be used for the downloading
Transport string `yaml:"transport,omitempty" json:"transport,omitempty"`
}

// ImageTranslation is a single translation config with optional prefix name
type ImageTranslation struct {

// Prefix allows to have several config-sets and distinguish them by using `prefix/imageName` notation. Optional.
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`

// Rules is a list of translations
Rules []TranslationRule `yaml:"translations" json:"translations"`

// Transports is a map of available transport profiles available for endpoints
Transports map[string]TransportProfile `yaml:"transports" json:"transports"`
}

// TransportProfile contains all the http transport settings
type TransportProfile struct {
// MaxRedirects is the maximum number of redirects that downloader is allowed to follow. Default is 9 (download fails on request #10)
MaxRedirects *int `yaml:"maxRedirects,omitempty" json:"maxRedirects,omitempty"`

// TLS config
TLS *TLSConfig `yaml:"tls,omitempty" json:"tls,omitempty"`

// TimeoutMilliseconds specifies a time limit in milliseconds for http(s) download request. <= 0 is no timeout (default)
TimeoutMilliseconds int `yaml:"timeout,omitempty" json:"timeout,omitempty"`

// Proxy server to use for downloading
Proxy string `yaml:"proxy,omitempty" json:"proxy,omitempty"`
}

// TLSConfig has the TLS transport parameters
type TLSConfig struct {
// Certificates - TLS certificates to use for connection
Certificates []TLSCertificate `yaml:"certificates,omitempty" json:"certificates,omitempty"`

// ServerName is used to verify the hostname on the returned certificates. Needed when url points to domain that
// differs from CN of certificate
ServerName string `yaml:"serverName,omitempty" json:"serverName,omitempty"`

// Insecure is a flag to bypass server certificate validation
Insecure bool `yaml:"insecure,omitempty" json:"insecure,omitempty"`
}

// TLSCertificate has the x509 certificate PEM data with optional PEM private key
type TLSCertificate struct {
// Cert certificate (PEM) block
Cert string `yaml:"cert,omitempty" json:"cert,omitempty"`

// Key - keypair (PEM) block
Key string `yaml:"key,omitempty" json:"key,omitempty"`
}

// TranslationConfig represents a single config (prefix + rule list) in a config-set
type TranslationConfig interface {

// Name returns the config name (any string identifier)
Name() string

Expand All @@ -62,7 +102,6 @@ type ConfigSource interface {

// ImageNameTranslator is the main translator interface
type ImageNameTranslator interface {

// LoadConfigs initializes translator with configs from supplied data sources. All previous mappings are discarded.
LoadConfigs(sources ...ConfigSource)

Expand Down
104 changes: 102 additions & 2 deletions pkg/imagetranslation/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ limitations under the License.
package imagetranslation

import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"os"
"regexp"
"strings"
"time"

"github.com/golang/glog"

Expand Down Expand Up @@ -54,6 +61,99 @@ func (t *imageNameTranslator) LoadConfigs(sources ...ConfigSource) {
t.translations = translations
}

func convertEndpoint(rule TranslationRule, config *ImageTranslation) utils.Endpoint {
profile, exists := config.Transports[rule.Transport]
if !exists {
return utils.Endpoint{
Url: rule.Url,
MaxRedirects: -1,
}
}
if profile.TimeoutMilliseconds < 0 {
profile.TimeoutMilliseconds = 0
}
maxRedirects := -1
if profile.MaxRedirects != nil {
maxRedirects = *profile.MaxRedirects
}

var tlsConfig *utils.TLSConfig
if profile.TLS != nil {
var certificates []utils.TLSCertificate
for i, record := range profile.TLS.Certificates {
var x509Certs []*x509.Certificate
var privateKey crypto.PrivateKey

for _, data := range [2]string{record.Key, record.Cert} {
dataBytes := []byte(data)
for {
block, rest := pem.Decode(dataBytes)
if block == nil {
break
}
if block.Type == "CERTIFICATE" {
c, err := x509.ParseCertificate(block.Bytes)
if err != nil {
glog.V(2).Infof("error decoding certificate #%d from transport profile %s", i, rule.Transport)
} else {
x509Certs = append(x509Certs, c)
}
} else if privateKey == nil && strings.HasSuffix(block.Type, "PRIVATE KEY") {
k, err := parsePrivateKey(block.Bytes)
if err != nil {
glog.V(2).Infof("error decoding private key #%d from transport profile %s", i, rule.Transport)
} else {
privateKey = k
}
}
dataBytes = rest
}
}

for _, c := range x509Certs {
certificates = append(certificates, utils.TLSCertificate{
Certificate: c,
PrivateKey: privateKey,
})
}
}

tlsConfig = &utils.TLSConfig{
ServerName: profile.TLS.ServerName,
Insecure: profile.TLS.Insecure,
Certificates: certificates,
}
}

return utils.Endpoint{
Url: rule.Url,
Timeout: time.Millisecond * time.Duration(profile.TimeoutMilliseconds),
Proxy: profile.Proxy,
ProfileName: rule.Transport,
MaxRedirects: maxRedirects,
TLS: tlsConfig,
}
}

func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey, *ecdsa.PrivateKey:
return key, nil
default:
return nil, errors.New("tls: found unknown private key type in PKCS#8 wrapping")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, nil
}

return nil, errors.New("tls: failed to parse private key")
}

// Translate implements ImageNameTranslator Translate
func (t *imageNameTranslator) Translate(name string) utils.Endpoint {
for _, translation := range t.translations {
Expand All @@ -67,7 +167,7 @@ func (t *imageNameTranslator) Translate(name string) utils.Endpoint {
}
for _, r := range translation.Rules {
if r.Name != "" && r.Name == unprefixedName {
return r.Endpoint
return convertEndpoint(r, translation)
}
}
if !t.AllowRegexp {
Expand All @@ -85,7 +185,7 @@ func (t *imageNameTranslator) Translate(name string) utils.Endpoint {
submatchIndexes := re.FindStringSubmatchIndex(unprefixedName)
if len(submatchIndexes) > 0 {
r.Url = string(re.ExpandString(nil, r.Url, unprefixedName, submatchIndexes))
return r.Endpoint
return convertEndpoint(r, translation)
}
}
}
Expand Down
22 changes: 5 additions & 17 deletions pkg/imagetranslation/translator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package imagetranslation

import (
"testing"

"github.com/Mirantis/virtlet/pkg/utils"
)

// TestTranslations tests how image names are translated with various translation rules
Expand All @@ -29,21 +27,15 @@ func TestTranslations(t *testing.T) {
Rules: []TranslationRule{
{
Regex: `^image(\d+)`,
Endpoint: utils.Endpoint{
Url: "http://example.net/image_$1.qcow2",
},
Url: "http://example.net/image_$1.qcow2",
},
{
Regex: `image(\d+)`,
Endpoint: utils.Endpoint{
Url: "http://example.net/alt_$1.qcow2",
},
Url: "http://example.net/alt_$1.qcow2",
},
{
Name: "image1",
Endpoint: utils.Endpoint{
Url: "https://example.net/base.qcow2",
},
Url: "https://example.net/base.qcow2",
},
},
},
Expand All @@ -52,15 +44,11 @@ func TestTranslations(t *testing.T) {
Rules: []TranslationRule{
{
Regex: `^linux/(\d+\.\d+)`,
Endpoint: utils.Endpoint{
Url: "http://acme.org/linux_$1.qcow2",
},
Url: "http://acme.org/linux_$1.qcow2",
},
{
Name: "linux/1",
Endpoint: utils.Endpoint{
Url: "https://acme.org/linux.qcow2",
},
Url: "https://acme.org/linux.qcow2",
},
},
},
Expand Down
Loading

0 comments on commit 1a0f691

Please sign in to comment.