Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add instance types #6564

Merged
merged 1 commit into from Nov 24, 2016
Merged

Add instance types #6564

merged 1 commit into from Nov 24, 2016

Conversation

perrito666
Copy link

Added InstanceTypes API endpoint that allows to learn
the available instance types in the model region/zone
to make better deployment decisions.

Tests are missing for 3 out of 4 providers implemented but I am keen to get the rest of the code reviewed as this needs to be merged soon.

This follows: https://docs.google.com/document/d/1m6iNWOMYyGwHbDbl8-u_VSZozeZQklxUOgM5t_0wbMQ/edit#heading=h.kc1rayquamxb

###QA
Run Tests.
Create an API client call that points to the controller and Perform the call for the different providers to obtain lists of available types. (This is intended to be used by the GUI)

@perrito666
Copy link
Author

!!build!!

@perrito666
Copy link
Author

!!try!!

@perrito666 perrito666 force-pushed the add_instance_types branch 2 times, most recently from 2035d3d to 1d9b90c Compare November 15, 2016 00:43
@perrito666
Copy link
Author

!!try!!

Copy link
Contributor

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looking good. I think we should get input from @urosj or someone on GUI about cost/currency details.

package provider

import (
names "gopkg.in/juju/names.v2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop alias (goimports is doing this to you too?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I just realized that, mm, Ill have to check what is going on with goimports so I can disable it


// NewAPI returns an API pointer.
func NewAPI(backend Backend, authorizer facade.Authorizer, envNew environs.NewEnvironFunc) (*API, error) {
if !authorizer.AuthModelManager() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instance types API is intended for users to call.

}, nil
}

type modelConfigAble interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use environs.ConfigGetter?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not satisfy what we need here (lacks err)

return params.InstanceTypesResults{}, errors.Trace(err)
}

for i, c := range cons.Constraints {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a TODO to cache the results. We need to avoid repeatedly querying the cloud, but cannot poll the cloud for reasons described in the spec.


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

var over9kCPUCores uint64 = 9001
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

@@ -0,0 +1,16 @@
package cloudsigma
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

@@ -0,0 +1,16 @@
package dummy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright
(etc.)

package dummy

import (
"github.com/juju/errors"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\n
(etc.)

return instances.InstanceTypesWithCostMetadata{}, errors.Trace(err)
}
for i, t := range iTypes {
iTypes[i].Cost = t.Cost / 1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. I must have been unclear, sorry.

What I meant was to include a divisor in the result struct, not to truncate/drop precision in the cost field. i.e. the results should include:

  • the cost (e.g. in deci-cents)
  • a divisor (e.g. of 1000) to get to dollars, or whatever the unit is
  • the currency/unit (e.g. $USD/h)

e.g. with cost=1234, divisor=1000, currency/unit=$USD/h, the GUI could display "1.234 $USD/h".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha, will do

}

result := make([]instances.InstanceType, len(resultUnique))
i := 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for i, it := range resultUnique {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once again, map. :p

Copy link
Contributor

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but let's get feedback from @urosj before committing to this.

InstanceTypes []InstanceType `json:"instance-types,omitempty"`
CostUnit string `json:"cost-unit,omitempty"`
CostCurrency string `json:"cost-currency,omitempty"`
Divisor uint64 `json:"divisor,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CostDivisor/cost-divisor please

CostCurrency string
// Divisor indicates a number that must be applied to InstanceType.Cost to obtain
// a number that is in CostUnit.
Divisor uint64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CostDivisor

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should document that setting (Cost)Divisor to zero will mean that the cost is treated as already being in specified unit, as the (cost-)divisor field will be omitted from the API response. i.e. the zero value is equivalent to 1.

@@ -0,0 +1,60 @@
package gce
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

@@ -0,0 +1,23 @@
package joyent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

@@ -0,0 +1,15 @@
package lxd
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright


var _ environs.InstanceTypesFetcher = (*environ)(nil)

func (e environ) InstanceTypes(c constraints.Value) (instances.InstanceTypesWithCostMetadata, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this one? I think the openstack impl is sufficient?

@@ -0,0 +1,16 @@
package vsphere
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

package vsphere

import (
"github.com/juju/errors"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\n

@@ -0,0 +1,15 @@
package rackspace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

package rackspace

import (
"github.com/juju/errors"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\n

costUnit := instanceTypes.CostUnit
costCurrency := instanceTypes.CostCurrency
divisor := instanceTypes.CostDivisor
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this next to the call to InstanceTypes ?


res[i] = params.InstanceTypesResult{
InstanceTypes: toParamsInstanceTypeResult(allTypes),
CostUnit: costUnit,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline? i.e. use instanceTypes.CostUnit instead of using a temp variable.


// InstanceTypesFetcher is an interface that allows for instance information from
// a provider to be obtained.
type InstanceTypesFetcher interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really worth having a separate interface type for this single method? Why not just make it part of Environ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separation of concerns

Copy link
Contributor

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's there LGTM, but the apiserver facade changes are missing.


// ModelInstanceTypesConstraint contains a constraint applied when filtering instance types.
type ModelInstanceTypesConstraint struct {
// Value, if specified, contains the constraints to filter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please change the name to Constraints, and tag to "constraints", to match the field in CloudInstanceTypesConstraints? That way one is a superset of the other.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping. This one is not so small, please don't forget about this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

InstanceTypes []InstanceType `json:"instance-types,omitempty"`
CostUnit string `json:"cost-unit,omitempty"`
CostCurrency string `json:"cost-currency,omitempty"`
// CostDivisor Will be present only when the Cost is not expressed in CostUnit.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Will/will/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}
result := make([]instances.InstanceType, len(types))
i := 0
for _, iType := range types {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@perrito666 like rogpeppe, I do the same when going from map to slice. Only because it's more compact and reads better (to me).

},
}
return &machineType, nil

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete empty line

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

)

var _ environs.InstanceTypesFetcher = (*environ)(nil)
var virtType = "kvm"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I use a const I cannot pass its addres afterwards (instances.InstanceType.VirtType is a pointer)

"github.com/juju/juju/environs"
"github.com/juju/juju/state"
"github.com/juju/juju/state/stateenvirons"
names "gopkg.in/juju/names.v2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move me, drop alias

package cloud

import (
"github.com/juju/errors"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move me

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

// EnvironConfigGetter implements environs.EnvironConfigGetter
// in terms of a *state.State.
type cloudEnvironConfigGetter struct {
*state.State
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only need state.CloudAccessor and the GetModel function. Backend (apiserver/cloud/backend.go) already embeds state.CloudAccessor, so you just need to add the GetModel method, and you can use Backend instead of State.

You could otherwise just ignore the ModelTag, and use the existing Backend.ControllerModel method.

I think it might be simpler though if you just pass the CloudSpec (or perhaps Environ?) into the common.InstanceTypes function? i.e. have the facades pull apart the args, get the appropriate model, cloud, and region, and construct a CloudSpec from them. You may as well then get the Environ then too, and avoid passing the backend in just to get ModelConfig?

if cons.Constraints != nil {
value = *cons.Constraints
}
backend := cloudEnvironConfigGetter{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please parse the cons.CloudTag here, and check that it is the same as the controller model's cloud. We may relax this later, but it's better to be strict to avoid having to deal with sloppily written clients later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


// InstanceTypes returns instance type information for the cloud and region
// in which the current model is deployed.
func (api *CloudAPI) InstanceTypes(cons params.CloudInstanceTypesConstraints) (params.InstanceTypesResults, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need a test or two for this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

itCons := common.NewInstanceTypeConstraints(backend, value, modelTag)
it, err := common.InstanceTypes(itCons)
if err != nil {
return params.InstanceTypesResults{}, errors.Trace(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An error due to one of them failing should not cause all of them to fail.

Either assume that common.InstanceTypes never fills in the Error field, and if it returns an error, have the caller (Cloud or MachineManager facade) fill it in; or change common.InstanceTypes to not have an error result.

environs.EnvironConfigGetter
ModelTag() names.ModelTag
Close() error
GetModel(names.ModelTag) (*state.Model, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please not state.Model, use an interface

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mirroring state, there is nothing I can do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't be so defeatist ;)

Look at ControllerModel in apiserver/cloud/backend.go for an example of how to do this. Anyway, I think this will be moot if you pass the Environ in?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that seconds after writing that and fixed it with ashim, and am now writing the tests

// InstanceTypes returns a list of the available instance types in the provider according
// to the passed constraints.
func InstanceTypes(cons instanceTypeConstraints) (params.InstanceTypesResult, error) {
m, err := cons.backend.GetModel(cons.modelTag)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that we're calling GetModel multiple times for what's always going to be the same model is a bit of a smell I think. As mentioned before, I think we should pass the CloudSpec (perhaps Environ?) in.

If this were split out, then it would be reasonable to fail the entire API call if we fail to get the CloudSpec in the case of the MachineManager facade. For the Cloud facade, it would be reasonable to fail the entire API call if we fail to get the Model; and then fail individual results if we can't get the CloudSpec (e.g. if an invalid CloudTag or region is specified).

Copy link
Contributor

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking better, thanks.

// information.
type InstanceTypeBackend interface {
environs.EnvironConfigGetter
Close() error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused, drop me. I don't think it's appropriate to hand off ownership/responsibility to InstanceTypes anyway.

Config() (*config.Config, error)
}

// NewModelEnvironConfigGetter can create
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please move this, modelEnvironConfigGetter, and InstanceTypeBackend to a separate file? It's only tangentially related to instance types. Also, InstanceTypeBackend should be renamed to be something to do with model/environ config.

It might be simpler just to have this as a struct of two functions? e.g.

type EnvironConfigGetterFuncs struct {
    ModelConfigFunc func() (*config.Config, error)
    CloudSpecFunc func(names.ModelTag) (environs.CloudSpec, error)
}

func (f EnvironConfigGetterFuncs) ModelConfig() (*config.Config, error) {
    return f.ModelConfigFunc()
}

...

Just a thought, feel free to ignore that.

The EnvironConfigGetter interface is kinda weird. Both methods should probably be taking a ModelTag...


// InstanceTypes returns a list of the available instance types in the provider according
// to the passed constraints.
func InstanceTypes(cons instanceTypeConstraints) (params.InstanceTypesResult, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pass around an unexported type?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

// InstanceTypes returns instance type information for the cloud and region
// in which the current model is deployed.
func (mm *MachineManagerAPI) InstanceTypes(cons params.ModelInstanceTypesConstraints) (params.InstanceTypesResults, error) {
st := mm.st.(*state.State)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do the same for this method as you did for the other one, and use interfaces (stateInterface).

jujutesting.Stub

results map[constraints.Value]instances.InstanceTypesWithCostMetadata
}

func (m *mockEnviron) InstanceTypes(c constraints.Value) (instances.InstanceTypesWithCostMetadata, error) {
fmt.Println(c)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d

return m, nil
}

func (s stateShim) Cloud(cloudName string) (cloud.Cloud, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these Cloud* methods aren't needed? stateShim embeds State, and State already has those methods

Copy link
Contributor

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM once remaining comments are addressed (those from this review and ones from before, unless we've already talked about them.)

"github.com/juju/juju/state/stateenvirons"
)

// EnvironConfigGetter implements environs.EnvironConfigGetter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cloudEnvironConfigGetter

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

return params.InstanceTypesResults{}, errors.Trace(err)
}
modelTag := c.ModelTag()
m, err := api.backend.GetModel(modelTag)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you end up with the same model as returned by ControllerModel

just do m, err := api.backend.ControllerModel()

then you can get rid of the ModelTag method from Backend

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

"github.com/juju/juju/environs/config"
)

// EnvironConfigGetterFuncs holds implements environs.EnvironConfigGetter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/holds //

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


// ModelInstanceTypesConstraint contains a constraint applied when filtering instance types.
type ModelInstanceTypesConstraint struct {
// Value, if specified, contains the constraints to filter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping. This one is not so small, please don't forget about this

Copy link
Member

@frankban frankban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good and works well. Just a couple of minor issues. Did you talk about opening the Cloud call up to anonymous users?

if m.Cloud() != cloudTag.Id() {
result[i] = params.InstanceTypesResult{Error: common.ServerError(errors.NotValidf("asking %s cloud information to %s cloud", cloudTag.Id(), m.Cloud()))}
continue
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd check also that cons.CloudRegion is not empty here. otherwise I guess the subsequent error would be ugly and not really informative. This case should also be tested.

) (environs.Environ, error) {
return &env, nil
}
r, err := machinemanager.InstanceTypes(&api, fakeEnvironGet, cons)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also test the case no constraints are passed? In this case I'd expect an empty result.

@@ -75,6 +75,10 @@ func instanceTypes(api *CloudAPI,
result[i] = params.InstanceTypesResult{Error: common.ServerError(errors.NotValidf("asking %s cloud information to %s cloud", cloudTag.Id(), m.Cloud()))}
continue
}
if cons.CloudRegion == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all clouds have regions. I'm not sure if the validation belongs here or not, but if we do validate, we need to check that the cloud supports regions before requiring that it's empty.

@axw
Copy link
Contributor

axw commented Nov 23, 2016

@frankban

Looks good and works well. Just a couple of minor issues. Did you talk about opening the Cloud call up to anonymous users?

Yes we have talked about it, but as we agreed we'll defer this bit. It's not feasible to make that change for 2.1.

Added InstanceTypes API endpoint that allows to learn
the available instance types in the model region/zone
to make better deployment decisions.
@perrito666
Copy link
Author

!!try!!

@perrito666
Copy link
Author

$$merge$$

@jujubot
Copy link
Collaborator

jujubot commented Nov 24, 2016

Status: merge request accepted. Url: http://juju-ci.vapour.ws:8080/job/github-merge-juju

@jujubot jujubot merged commit acfb0ce into juju:develop Nov 24, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants