Skip to content

Commit

Permalink
Enable consumption of docker resources in caas.
Browse files Browse the repository at this point in the history
  • Loading branch information
Veebers committed Jul 18, 2018
1 parent 23a6559 commit 46191b4
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 26 deletions.
6 changes: 1 addition & 5 deletions apiserver/allfacades.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,7 @@ func AllFacades() *facade.Registry {
reg("RemoteRelations", 1, remoterelations.NewStateRemoteRelationsAPI)

reg("Resources", 1, resources.NewPublicFacade)
regHookContext(
"ResourcesHookContext", 1,
resourceshookcontext.NewHookContextFacade,
reflect.TypeOf(&resourceshookcontext.UnitFacade{}),
)
reg("ResourcesHookContext", 1, resourceshookcontext.NewStateFacade)

reg("Resumer", 2, resumer.NewResumerAPI)
reg("RetryStrategy", 1, retrystrategy.NewRetryStrategyAPI)
Expand Down
46 changes: 46 additions & 0 deletions apiserver/facades/agent/resourceshookcontext/unitfacade.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package resourceshookcontext

import (
"github.com/juju/errors"
"gopkg.in/juju/names.v2"

"github.com/juju/juju/apiserver/common"
"github.com/juju/juju/apiserver/facade"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/resource"
"github.com/juju/juju/resource/api"
Expand All @@ -22,6 +24,50 @@ func NewHookContextFacade(st *state.State, unit *state.Unit) (interface{}, error
return NewUnitFacade(&resourcesUnitDataStore{res, unit}), nil
}

// NewStateFacade provides the signature to register this resource facade
func NewStateFacade(ctx facade.Context) (*UnitFacade, error) {
authorizer := ctx.Auth()
st := ctx.State()

if !authorizer.AuthUnitAgent() && !authorizer.AuthApplicationAgent() {
return nil, common.ErrPerm
}

var (
unit *state.Unit
err error
)
switch tag := authorizer.GetAuthTag().(type) {
case names.UnitTag:
unit, err = st.Unit(tag.Id())
if err != nil {
return nil, errors.Trace(err)
}
case names.ApplicationTag:
// Allow application access for K8s units. As they are all homogeneous any of the units will suffice.
app, err := st.Application(tag.Id())
if err != nil {
return nil, errors.Trace(err)
}
allUnits, err := app.AllUnits()
if err != nil {
return nil, errors.Trace(err)
}
if len(allUnits) <= 0 {
return nil, errors.Errorf("failed to get units for app: %s", app.Name())
}
unit = allUnits[0]
default:
return nil, errors.Errorf("expected names.UnitTag or names.ApplicationTag, got %T", tag)
}

res, err := st.Resources()
if err != nil {
return nil, errors.Trace(err)
}
return NewUnitFacade(&resourcesUnitDataStore{res, unit}), nil
}

// resourcesUnitDatastore is a shim to elide serviceName from
// ListResources.
type resourcesUnitDataStore struct {
Expand Down
1 change: 1 addition & 0 deletions apiserver/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ func (h *guiHandler) serveConfig(w http.ResponseWriter, req *http.Request) {
"base": base,
"bakeryEnabled": ctrl.IdentityURL() != "",
"controllerSocket": "/api",
"charmstoreURL": ctrl.CharmStoreURL(),
"host": req.Host,
"socket": "/model/$uuid/api",
// staticURL holds the root of the static hierarchy, hence why the
Expand Down
5 changes: 4 additions & 1 deletion apiserver/gui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
jc "github.com/juju/testing/checkers"
"github.com/juju/version"
gc "gopkg.in/check.v1"
"gopkg.in/juju/charmrepo.v3/csclient"

agenttools "github.com/juju/juju/agent/tools"
"github.com/juju/juju/apiserver"
Expand Down Expand Up @@ -589,6 +590,7 @@ var config = {
bakeryEnabled: {{.bakeryEnabled}},
host: '{{.host}}',
controllerSocket: '{{.controllerSocket}}',
charmstoreURL: '{{.charmstoreURL}}',
socket: '{{.socket}}',
staticURL: '{{.staticURL}}',
uuid: '{{.uuid}}',
Expand All @@ -610,11 +612,12 @@ var config = {
bakeryEnabled: false,
host: '%[2]s',
controllerSocket: '/api',
charmstoreURL: '%[6]s',
socket: '/model/$uuid/api',
staticURL: '/gui/%[3]s',
uuid: '%[1]s',
version: '%[4]s'
};`, test.expectedUUID, serverHost, hash, jujuversion.Current, test.expectedBaseURL)
};`, test.expectedUUID, serverHost, hash, jujuversion.Current, test.expectedBaseURL, csclient.ServerURL)

// Make a request for the Juju GUI config.
resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
Expand Down
10 changes: 6 additions & 4 deletions charmstore/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@ func (c Client) GetResource(req ResourceRequest) (data ResourceData, err error)
}
}()
data.ReadCloser = resData.ReadCloser
fpHash := data.Resource.Fingerprint.String()
if resData.Hash != fpHash {
return ResourceData{},
errors.Errorf("fingerprint for data (%s) does not match fingerprint in metadata (%s)", resData.Hash, fpHash)
if data.Resource.Type == charmresource.TypeFile {
fpHash := data.Resource.Fingerprint.String()
if resData.Hash != fpHash {
return ResourceData{},
errors.Errorf("fingerprint for data (%s) does not match fingerprint in metadata (%s)", resData.Hash, fpHash)
}
}
return data, nil
}
Expand Down
38 changes: 38 additions & 0 deletions charmstore/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,44 @@ func (s *ClientSuite) TestGetResource(c *gc.C) {
s.wrapper.stub.CheckCall(c, 2, "GetResource", params.EdgeChannel, req.Charm, req.Name, req.Revision)
}

func (s *ClientSuite) TestGetResourceDockerType(c *gc.C) {
fp, err := resource.GenerateFingerprint(strings.NewReader("data"))
c.Assert(err, jc.ErrorIsNil)
rc := ioutil.NopCloser(strings.NewReader("data"))
s.wrapper.ReturnGetResource = csclient.ResourceData{
ReadCloser: rc,
Hash: fp.String(),
}
apiRes := params.Resource{
Name: "mysql_image",
Type: "docker",
Description: "something",
Revision: 2,
Fingerprint: resource.Fingerprint{}.Bytes(),
Size: 4,
}
s.wrapper.ReturnResourceMeta = apiRes

client, err := newCachingClient(s.cache, "", s.wrapper.makeWrapper)
c.Assert(err, jc.ErrorIsNil)

req := ResourceRequest{
Charm: charm.MustParseURL("cs:mysql"),
Channel: params.EdgeChannel,
Name: "mysql_image",
Revision: 5,
}
data, err := client.GetResource(req)
c.Assert(err, jc.ErrorIsNil)
expected, err := params.API2Resource(apiRes)
c.Assert(err, jc.ErrorIsNil)
c.Check(data.Resource, gc.DeepEquals, expected)
c.Check(data.ReadCloser, gc.DeepEquals, rc)
// call #0 is a call to makeWrapper
s.wrapper.stub.CheckCall(c, 1, "ResourceMeta", params.EdgeChannel, req.Charm, req.Name, req.Revision)
s.wrapper.stub.CheckCall(c, 2, "GetResource", params.EdgeChannel, req.Charm, req.Name, req.Revision)
}

func (s *ClientSuite) TestResourceInfo(c *gc.C) {
fp, err := resource.GenerateFingerprint(strings.NewReader("data"))
c.Assert(err, jc.ErrorIsNil)
Expand Down
6 changes: 5 additions & 1 deletion controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,11 @@ func (c Config) Features() set.Strings {

// CharmStoreURL returns the URL to use for charmstore api calls.
func (c Config) CharmStoreURL() string {
return c.asString(CharmStoreURL)
url := c.asString(CharmStoreURL)
if url == "" {
return csclient.ServerURL
}
return url
}

// ControllerUUID returns the uuid for the model's controller.
Expand Down
6 changes: 3 additions & 3 deletions core/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (
// DockerImageDetails holds the details for a Docker resource type.
type DockerImageDetails struct {
// RegistryPath holds the path of the Docker image (including host and sha256) in a docker registry.
RegistryPath string `json:"ImagePath"`
RegistryPath string `json:"ImageName" yaml:"registrypath"`

// Username holds the username used to gain access to a non-public image.
Username string `json:"Username,omitempty"`
Username string `json:"Username" yaml:"username"`

// Password holds the password used to gain access to a non-public image.
Password string `json:"Password,omitempty"`
Password string `json:"Password,omitempty" yaml:"password"`
}

var validDockerImageRegExp = regexp.MustCompile(`^([A-Za-z\.]+/)?(([A-Za-z-_\.])+/?)+((@sha256){0,1}:[A-Za-z0-9-_\.]+)?$`)
Expand Down
14 changes: 14 additions & 0 deletions core/resources/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package resources_test

import (
"encoding/json"

jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"

Expand All @@ -25,3 +27,15 @@ func (s *ResourceSuite) TestInvalidRegistryPath(c *gc.C) {
err := resources.ValidateDockerRegistryPath("sha256:deedbeaf")
c.Assert(err, gc.ErrorMatches, "docker image path .* not valid")
}

func (s *ResourceSuite) TestDockerImageDetailsUnmarshal(c *gc.C) {
data := []byte(`{"ImageName":"testing@sha256:beef-deed","Username":"docker-registry","Password":"fragglerock"}`)
var result resources.DockerImageDetails
err := json.Unmarshal(data, &result)
c.Assert(err, jc.ErrorIsNil)
c.Assert(result, gc.DeepEquals, resources.DockerImageDetails{
RegistryPath: "testing@sha256:beef-deed",
Username: "docker-registry",
Password: "fragglerock",
})
}
2 changes: 1 addition & 1 deletion dependencies.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ gopkg.in/goose.v2 git 36df5d12fc6d0ef1c102e1c18a22ddf345b18821 2018-03-22T12:54:
gopkg.in/httprequest.v1 git 1a21782420ea13c3c6fb1d03578f446b3248edb1 2018-03-08T16:26:44Z
gopkg.in/ini.v1 git 776aa739ce9373377cd16f526cdf06cb4c89b40f 2016-02-22T23:24:41Z
gopkg.in/juju/blobstore.v2 git 51fa6e26128d74e445c72d3a91af555151cc3654 2016-01-25T02:37:03Z
gopkg.in/juju/charm.v6 git e6a4837dfe5ac4394d9861a97c45a887448bdc3c 2018-07-09T02:22:58Z
gopkg.in/juju/charm.v6 git 52d8af8e45d911a36f072826d1229d83ecdb958d 2018-07-15T21:39:02Z
gopkg.in/juju/charmrepo.v2 git 653bbd81990d2d7d48e0fb931a3b0f52c694572f 2017-11-14T18:40:45Z
gopkg.in/juju/charmrepo.v3 git 8bb46aa94f3c679069e80fe35aac6d0581096d65 2018-06-29T07:07:28Z
gopkg.in/juju/charmstore.v5 git d93d0fd2b81b8230bdf9b1be91b50538dd8782fa 2018-06-28T08:11:03Z
Expand Down
6 changes: 5 additions & 1 deletion resource/context/internal/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ type Content struct {
Fingerprint charmresource.Fingerprint
}

// verify ensures that the actual resource content details match
// Verify ensures that the actual resource content details match
// the expected ones.
func (c Content) Verify(size int64, fp charmresource.Fingerprint) error {
if size != c.Size {
return errors.Errorf("resource size does not match expected (%d != %d)", size, c.Size)
}
// Only verify a finger print if it's set (i.e not for docker image details).
if c.Fingerprint.IsZero() {
return nil
}
if !bytes.Equal(fp.Bytes(), c.Fingerprint.Bytes()) {
return errors.Errorf("resource fingerprint does not match expected (%q != %q)", fp, c.Fingerprint)
}
Expand Down
28 changes: 18 additions & 10 deletions state/resources_state_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package state

import (
"bytes"
"encoding/json"
"fmt"
"io"
"path"
Expand All @@ -16,7 +17,6 @@ import (
charmresource "gopkg.in/juju/charm.v6/resource"
"gopkg.in/juju/names.v2"
"gopkg.in/mgo.v2/txn"
"gopkg.in/yaml.v2"

"github.com/juju/juju/core/resources"
"github.com/juju/juju/resource"
Expand Down Expand Up @@ -286,8 +286,11 @@ func (st resourceState) storeResource(res resource.Resource, r io.Reader) error
case charmresource.TypeDocker:
var dockerDetails resources.DockerImageDetails
respBuf := new(bytes.Buffer)
respBuf.ReadFrom(r)
err = yaml.Unmarshal(respBuf.Bytes(), &dockerDetails)
_, err := respBuf.ReadFrom(r)
if err != nil {
return errors.Trace(err)
}
err = json.Unmarshal(respBuf.Bytes(), &dockerDetails)
if err != nil {
return errors.Trace(err)
}
Expand Down Expand Up @@ -343,14 +346,19 @@ func (st resourceState) OpenResource(applicationID, name string) (resource.Resou
if err != nil {
return resource.Resource{}, nil, errors.Annotate(err, "while retrieving resource data")
}
if resourceInfo.Type == charmresource.TypeDocker {
// Resource size only found at this stage in time.
switch resourceInfo.Type {
case charmresource.TypeDocker:
// Resource size only found at this stage in time as it's a response from the charmstore, not a stored file.
// Store it as it's used later for verification (in a separate call than this one)
resourceInfo.Size = resSize

}
if resSize != resourceInfo.Size {
msg := "storage returned a size (%d) which doesn't match resource metadata (%d)"
return resource.Resource{}, nil, errors.Errorf(msg, resSize, resourceInfo.Size)
if err := st.persist.SetResource(resourceInfo); err != nil {
return resource.Resource{}, nil, errors.Annotate(err, "failed to update resource details with docker detail size")
}
case charmresource.TypeFile:
if resSize != resourceInfo.Size {
msg := "storage returned a size (%d) which doesn't match resource metadata (%d)"
return resource.Resource{}, nil, errors.Errorf(msg, resSize, resourceInfo.Size)
}
}

return resourceInfo, resourceReader, nil
Expand Down

0 comments on commit 46191b4

Please sign in to comment.