Skip to content

Commit

Permalink
Support snap store proxy setup via the snap-store-proxy-url config opt.
Browse files Browse the repository at this point in the history
  • Loading branch information
achilleasa committed Sep 6, 2019
1 parent 59cbe59 commit ee10204
Show file tree
Hide file tree
Showing 13 changed files with 275 additions and 7 deletions.
1 change: 1 addition & 0 deletions apiserver/facades/agent/provisioner/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ func (p *ProvisionerAPI) ContainerConfig() (params.ContainerConfig, error) {
result.SnapProxy = config.SnapProxySettings()
result.SnapStoreAssertions = config.SnapStoreAssertions()
result.SnapStoreProxyID = config.SnapStoreProxy()
result.SnapStoreProxyURL = config.SnapStoreProxyURL()
result.CloudInitUserData = config.CloudInitUserData()
result.ContainerInheritProperties = config.ContainerInheritProperties()
return result, nil
Expand Down
7 changes: 7 additions & 0 deletions apiserver/facades/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25766,6 +25766,9 @@
"snap-store-proxy-id": {
"type": "string"
},
"snap-store-proxy-url": {
"type": "string"
},
"ssl-hostname-verification": {
"type": "boolean"
}
Expand All @@ -25781,6 +25784,7 @@
"snap-proxy",
"snap-store-assertions",
"snap-store-proxy-id",
"snap-store-proxy-url",
"apt-mirror",
"UpdateBehavior"
]
Expand Down Expand Up @@ -27491,6 +27495,9 @@
},
"snap-store-id": {
"type": "string"
},
"snap-store-proxy-url": {
"type": "string"
}
},
"additionalProperties": false,
Expand Down
1 change: 1 addition & 0 deletions apiserver/params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ type ContainerConfig struct {
SnapProxy proxy.Settings `json:"snap-proxy"`
SnapStoreAssertions string `json:"snap-store-assertions"`
SnapStoreProxyID string `json:"snap-store-proxy-id"`
SnapStoreProxyURL string `json:"snap-store-proxy-url"`
AptMirror string `json:"apt-mirror"`
CloudInitUserData map[string]interface{} `json:"cloudinit-userdata,omitempty"`
ContainerInheritProperties string `json:"container-inherit-properties,omitempty"`
Expand Down
26 changes: 19 additions & 7 deletions cloudconfig/cloudinit/cloudinit_ubuntu.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/juju/errors"
"github.com/juju/juju/core/snap"
jujupackaging "github.com/juju/juju/packaging"
"github.com/juju/packaging"
"github.com/juju/packaging/config"
Expand Down Expand Up @@ -323,15 +324,26 @@ func (cfg *ubuntuCloudConfig) updateProxySettings(proxyCfg PackageManagerProxyCo
}

// Configure snap store proxy
if proxyCfg.SnapStoreAssertions() != "" && proxyCfg.SnapStoreProxyID() != "" {
cfg.AddBootCmd(fmt.Sprintf(
`printf '%%s\n' %s > %s`,
utils.ShQuote(proxyCfg.SnapStoreAssertions()),
"/etc/snap.assertions"))
cfg.AddBootCmd("snap ack /etc/snap.assertions")
cfg.AddBootCmd("snap set core proxy.store=" + proxyCfg.SnapStoreProxyID())
if proxyURL := proxyCfg.SnapStoreProxyURL(); proxyURL != "" {
assertions, storeID, err := snap.LookupAssertions(proxyURL)
if err != nil {
return err
}
logger.Infof("auto-detected snap store assertions from proxy")
logger.Infof("auto-detected snap store ID as %q", storeID)
cfg.genSnapStoreProxyCmds(assertions, storeID)
} else if proxyCfg.SnapStoreAssertions() != "" && proxyCfg.SnapStoreProxyID() != "" {
cfg.genSnapStoreProxyCmds(proxyCfg.SnapStoreAssertions(), proxyCfg.SnapStoreProxyID())
}

return nil
}

func (cfg *ubuntuCloudConfig) genSnapStoreProxyCmds(assertions, storeID string) {
cfg.AddBootCmd(fmt.Sprintf(
`printf '%%s\n' %s > %s`,
utils.ShQuote(assertions),
"/etc/snap.assertions"))
cfg.AddBootCmd("snap ack /etc/snap.assertions")
cfg.AddBootCmd("snap set core proxy.store=" + storeID)
}
1 change: 1 addition & 0 deletions cloudconfig/cloudinit/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ type PackageManagerProxyConfig interface {
SnapProxy() proxy.Settings
SnapStoreAssertions() string
SnapStoreProxyID() string
SnapStoreProxyURL() string
}

// Makes two more advanced package commands available
Expand Down
12 changes: 12 additions & 0 deletions cloudconfig/instancecfg/instancecfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ type InstanceConfig struct {
// a snap store proxy.
SnapStoreProxyID string

// SnapStoreProxyURL specifies the address of the snap store proxy. If
// specified instead of the assertions/storeID settings above, juju can
// directly contact the proxy to retrieve the assertions and store ID.
SnapStoreProxyURL string

// The type of Simple Stream to download and deploy on this instance.
ImageStream string

Expand Down Expand Up @@ -859,6 +864,11 @@ type ProxyConfiguration struct {
// assertion list that must be passed to snapd before it can connect to
// a snap store proxy.
SnapStoreProxyID string

// SnapStoreProxyURL specifies the address of the snap store proxy. If
// specified instead of the assertions/storeID settings above, juju can
// directly contact the proxy to retrieve the assertions and store ID.
SnapStoreProxyURL string
}

// proxyConfigurationFromEnv populates a ProxyConfiguration object from an
Expand All @@ -872,6 +882,7 @@ func proxyConfigurationFromEnv(cfg *config.Config) ProxyConfiguration {
Snap: cfg.SnapProxySettings(),
SnapStoreAssertions: cfg.SnapStoreAssertions(),
SnapStoreProxyID: cfg.SnapStoreProxy(),
SnapStoreProxyURL: cfg.SnapStoreProxyURL(),
}
}

Expand Down Expand Up @@ -906,6 +917,7 @@ func PopulateInstanceConfig(icfg *InstanceConfig,
icfg.SnapProxySettings = proxyCfg.Snap
icfg.SnapStoreAssertions = proxyCfg.SnapStoreAssertions
icfg.SnapStoreProxyID = proxyCfg.SnapStoreProxyID
icfg.SnapStoreProxyURL = proxyCfg.SnapStoreProxyURL
icfg.EnableOSRefreshUpdate = enableOSRefreshUpdates
icfg.EnableOSUpgrade = enableOSUpgrade
icfg.CloudInitUserData = cloudInitUserData
Expand Down
4 changes: 4 additions & 0 deletions cloudconfig/userdatacfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ type packageManagerProxySettings struct {
snapProxy proxy.Settings
snapStoreAssertions string
snapStoreProxyID string
snapStoreProxyURL string
}

// AptProxy implements cloudinit.PackageManagerConfig.
Expand All @@ -226,3 +227,6 @@ func (p packageManagerProxySettings) SnapStoreAssertions() string { return p.sna

// SnapStoreProxyID implements cloudinit.PackageManagerConfig.
func (p packageManagerProxySettings) SnapStoreProxyID() string { return p.snapStoreProxyID }

// SnapStoreProxyURL implements cloudinit.PackageManagerConfig.
func (p packageManagerProxySettings) SnapStoreProxyURL() string { return p.snapStoreProxyURL }
1 change: 1 addition & 0 deletions cloudconfig/userdatacfg_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ func (w *unixConfigure) ConfigureJuju() error {
snapProxy: w.icfg.SnapProxySettings,
snapStoreAssertions: w.icfg.SnapStoreAssertions,
snapStoreProxyID: w.icfg.SnapStoreProxyID,
snapStoreProxyURL: w.icfg.SnapStoreProxyURL,
},
w.icfg.EnableOSRefreshUpdate,
w.icfg.EnableOSUpgrade,
Expand Down
1 change: 1 addition & 0 deletions container/broker/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,5 +258,6 @@ func proxyConfigurationFromContainerCfg(cfg params.ContainerConfig) instancecfg.
Snap: cfg.SnapProxy,
SnapStoreAssertions: cfg.SnapStoreAssertions,
SnapStoreProxyID: cfg.SnapStoreProxyID,
SnapStoreProxyURL: cfg.SnapStoreProxyURL,
}
}
94 changes: 94 additions & 0 deletions core/snap/assertions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2019 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package snap

import (
"context"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"time"

"github.com/juju/errors"
)

// LookupAssertions attempts to download an assertion list from the snap store
// proxy located at proxyURL and locate the store ID associated with the
// specified proxyURL.
//
// If the local snap store proxy instance is operating in an air-gapped
// environment, downloading the assertion list from the proxy will not be
// possible and an appropriate error will be returned.
func LookupAssertions(proxyURL string) (assertions, storeID string, err error) {
u, err := url.Parse(proxyURL)
if err != nil {
return "", "", errors.Annotate(err, "proxy URL not valid")
}
if u.Scheme != "http" && u.Scheme != "https" {
return "", "", errors.NotValidf("proxy URL scheme %q", u.Scheme)
}

// Make sure to redact user/pass when including the proxy URL in error messages
u.User = nil
noCredsProxyURL := u.String()

pathURL, _ := url.Parse("/v2/auth/store/assertions")
req, _ := http.NewRequest("GET", u.ResolveReference(pathURL).String(), nil)
ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelFn()

res, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return "", "", errors.Annotatef(err, "could not contact snap store proxy at %q. If using an air-gapped proxy you must manually provide the assertions file and store ID", noCredsProxyURL)
}
defer func() { _ = res.Body.Close() }()
if res.StatusCode != http.StatusOK {
return "", "", errors.Annotatef(err, "could not retrieve assertions from proxy at %q; proxy replied with unexpected HTTP status code %d", noCredsProxyURL, res.StatusCode)
}

data, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", "", errors.Annotatef(err, "could not read assertions response from proxy at %q", noCredsProxyURL)
}
assertions = string(data)
if storeID, err = findStoreID(assertions, u); err != nil {
return "", "", errors.Trace(err)
}

return assertions, storeID, nil
}

var storeInAssertionRE = regexp.MustCompile(`(?is)type: store.*?store: ([a-zA-Z0-9]+).*?url: (https?://[^\s]+)`)

func findStoreID(assertions string, proxyURL *url.URL) (string, error) {
var storeID string
for _, match := range storeInAssertionRE.FindAllStringSubmatch(assertions, -1) {
if len(match) != 3 {
continue
}

// Found store assertion but not for the URL provided
storeURL, err := url.Parse(match[2])
if err != nil {
continue
}
if storeURL.Host != proxyURL.Host {
continue
}

// Found same URL but different store ID
if storeID != "" && match[1] != storeID {
return "", errors.Errorf("assertions response from proxy at %q is ambiguous as it contains multiple entries with the same proxy URL but different store ID", proxyURL)
}

storeID = match[1]
}

if storeID == "" {
return "", errors.NotFoundf("store ID in assertions response from proxy at %q", proxyURL)
}

return storeID, nil
}
106 changes: 106 additions & 0 deletions core/snap/assertions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2019 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package snap_test

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"

"github.com/juju/juju/core/snap"
gc "gopkg.in/check.v1"
)

var (
_ = gc.Suite(&SnapSuite{})

snapProxyResponse = `
type: account-key
authority-id: canonical
revision: 2
public-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3m8ul
account-id: canonical
name: store
since: 2016-04-01T00:00:00.0Z
body-length: 717
sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
DATA...
MORE DATA...
type: account
authority-id: canonical
account-id: 1234567890367OdMqoW9YLp3e0EgakQf
display-name: John Doe
timestamp: 2019-05-10T13:12:32.878905Z
username: jdoe
validation: unproven
sign-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3m8ul
DATA...
type: store
authority-id: canonical
store: 1234567890STOREIDENTIFIER0123456
operator-id: 0123456789067OdMqoW9YLp3e0EgakQf
timestamp: 2019-08-27T12:20:45.166790Z
url: $store-url
sign-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3m8ul
DATA...
DATA...
type: store
authority-id: canonical
store: OTHER
operator-id: 0123456789067OdMqoW9YLp3e0EgakQf
timestamp: 2019-08-27T12:20:45.166790Z
url: $other-url/
sign-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3m8ul
DATA...
DATA...
`
)

type SnapSuite struct {
}

func (s *SnapSuite) TestLookupAssertions(c *gc.C) {
var (
srv *httptest.Server
assertionsRes string
)
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(assertionsRes))
}))
assertionsRes = strings.Replace(snapProxyResponse, "$store-url", srv.URL, -1)
defer srv.Close()

assertions, storeID, err := snap.LookupAssertions(srv.URL)
c.Assert(err, gc.IsNil)
c.Assert(assertions, gc.Equals, assertionsRes)
c.Assert(storeID, gc.Equals, "1234567890STOREIDENTIFIER0123456")
}

func (s *SnapSuite) TestConfigureProxyFromURLWithAmbiguousAssertions(c *gc.C) {
var (
srv *httptest.Server
assertionsRes string
)
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(assertionsRes))
}))
assertionsRes = strings.Replace(snapProxyResponse, "$store-url", srv.URL, -1)
assertionsRes = strings.Replace(assertionsRes, "$other-url", srv.URL, -1)
defer srv.Close()

// Make sure that we don't leak any credentials in error messages
srvURLWithPassword := fmt.Sprintf("http://42:secret@%s", srv.Listener.Addr())
_, _, err := snap.LookupAssertions(srvURLWithPassword)
expErr := fmt.Sprintf(`assertions response from proxy at "%s" is ambiguous as it contains multiple entries with the same proxy URL but different store ID`, srv.URL)
c.Assert(err, gc.ErrorMatches, expErr)
}
14 changes: 14 additions & 0 deletions core/snap/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2019 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package snap_test

import (
"testing"

gc "gopkg.in/check.v1"
)

func TestAll(t *testing.T) {
gc.TestingT(t)
}
Loading

0 comments on commit ee10204

Please sign in to comment.