diff --git a/CHANGELOG.md b/CHANGELOG.md index b7aaec4e..96d32317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- GraphQL API and playground (#117) +- `spec.register-date` and `spec.retire-date` fields to `Machine` (#116). +- REST API to edit `retire-date` (#116). +- `status.duration` field to `Machine` (#116). + +### Changed +- Update etcdutil to 1.3.1. + ## [0.24] - 2018-10-25 ### Changed diff --git a/docs/api.md b/docs/api.md index daee74bd..78e23e6f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,8 @@ REST API ======== +For GraphQL API, see [graphql.md](graphql.md). + * [PUT /api/v1/config/ipam](#putipam) * [GET /api/v1/config/ipam](#getipam) * [PUT /api/v1/config/dhcp](#putdhcp) diff --git a/docs/graphql.md b/docs/graphql.md index 5eaae3c6..a9125fdf 100644 --- a/docs/graphql.md +++ b/docs/graphql.md @@ -1,9 +1,102 @@ GraphQL API =========== -Sabakan provides GraphQL API at `/graphql` HTTP endpoint. +Sabakan provides [GraphQL][] API at `/graphql` HTTP endpoint. Schema ------ See [gql/schema.graphql](../gql/schema.graphql). + +Playground +---------- + +If [sabakan](sabakan.md) starts with `-enable-playground` command-line flag, +it serves a web-based playground for GraphQL API at `/playground` HTTP endpoint. + +Example +------- + +Searching machines matching the these conditions: + +* the machine has a label whose key is "datacenter" and value is "dc1". +* the machine's current state is _healthy_. +* the machine is not in rack 1. + +can be done with a GraphQL query and variables as follows: + +### Query + +``` +query search($having: MachineParams = null, $notHaving: MachineParams = null) { + searchMachines(having: $having, notHaving: $notHaving) { + spec { + serial + labels { + name + value + } + ipv4 + rack + } + status { + state + timestamp + duration + } + } +} +``` + +### Variables + +```json +{ + "having": { + "labels": [ + {"name": "datacenter", "value": "dc1"} + ], + "states": ["HEALTHY"] + }, + "notHaving": { + "racks": [1] + } +} +``` + +### Result + +```json +{ + "data": { + "searchMachines": [ + { + "spec": { + "serial": "00000004", + "labels": [ + { + "name": "datacenter", + "value": "dc1" + }, + { + "name": "product", + "value": "vm" + } + ], + "ipv4": [ + "10.0.0.104" + ], + "rack": 0 + }, + "status": { + "state": "HEALTHY", + "timestamp": "2018-11-26T09:17:20Z", + "duration": 21678.990289 + } + } + ] + } +} +``` + +[GraphQL]: https://graphql.org/ diff --git a/docs/sabakan.md b/docs/sabakan.md index 3025536e..8c587a2b 100644 --- a/docs/sabakan.md +++ b/docs/sabakan.md @@ -4,6 +4,8 @@ sabakan Usage ----- +See [specification of etcdutil](https://github.com/cybozu-go/etcdutil/blob/master/README.md#specifications) for etcd connection flags and parameters. + ```console $ sabakan -h Usage of sabakan: @@ -17,22 +19,8 @@ Usage of sabakan: directory to store files (default "/var/lib/sabakan") -dhcp-bind string bound ip addresses and port for dhcp server (default "0.0.0.0:10067") - -etcd-endpoints string - comma-separated URLs of the backend etcd endpoints (default "http://localhost:2379") - -etcd-password string - password for etcd authentication - -etcd-prefix string - etcd prefix (default "/sabakan/") - -etcd-timeout string - dial timeout to etcd (default "2s") - -etcd-tls-ca string - path to CA bundle used to verify certificates of etcd servers - -etcd-tls-cert string - path to my certificate used to identify myself to etcd servers - -etcd-tls-key string - path to my key used to identify myself to etcd servers - -etcd-username string - username for etcd authentication + -enable-playground + enable GraphQL playground -http string : (default "0.0.0.0:10080") -ipxe-efi-path string @@ -45,23 +33,16 @@ Usage of sabakan: Log level [critical,error,warning,info,debug] ``` -Option | Default value | Description ------- | ------------- | ----------- -`advertise-url` | "" | Public URL to access this server. Required. -`allow-ips` | `127.0.0.1,::1` | comma-separated IPs allowed to change resources -`config-file` | "" | If given, configurations are read from the file. -`data-dir` | `/var/lib/sabakan` | Directory to store files. -`dhcp-bind` | `0.0.0.0:10067` | bound ip addresses and port dhcp server -`etcd-endpoints` | `http://127.0.0.1:2379, http://127.0.0.1:4001` | comma-separated URLs of the backend etcd -`etcd-password` | "" | password for etcd authentication -`etcd-prefix` | `/sabakan` | etcd prefix -`etcd-timeout` | `2s` | dial timeout to etcd -`etcd-tls-ca` | "" | Path to CA bundle used to verify certificates of etcd endpoints. -`etcd-tls-cert` | "" | Path to my certificate used to identify myself to etcd servers. -`etcd-tls-key` | "" | Path to my key used to identify myself to etcd servers. -`etcd-username` | "" | username for etcd authentication -`http` | `0.0.0.0:10080` | Listen IP:Port number -`ipxe-efi-path` | `/usr/lib/ipxe/ipxe.efi` | path to ipxe.efi +Option | Default value | Description +------------------- | ------------------------ | ----------- +`advertise-url` | "" | Public URL to access this server. Required. +`allow-ips` | `127.0.0.1,::1` | Comma-separated IPs allowed to change resources. +`config-file` | "" | If given, configurations are read from the file. +`data-dir` | `/var/lib/sabakan` | Directory to store files. +`dhcp-bind` | `0.0.0.0:10067` | IP address and port number of DHCP server. +`enable-playground` | false | Enable GraphQL playground service. +`http` | `0.0.0.0:10080` | IP address and port number of HTTP server. +`ipxe-efi-path` | `/usr/lib/ipxe/ipxe.efi` | Path to ipxe.efi . Config file ----------- diff --git a/gql/generated.go b/gql/generated.go index 0ade828c..6c9e3f72 100644 --- a/gql/generated.go +++ b/gql/generated.go @@ -32,7 +32,7 @@ type Config struct { type ResolverRoot interface { BMC() BMCResolver - Machine() MachineResolver + MachineSpec() MachineSpecResolver MachineStatus() MachineStatusResolver Query() QueryResolver } @@ -52,6 +52,11 @@ type ComplexityRoot struct { } Machine struct { + Spec func(childComplexity int) int + Status func(childComplexity int) int + } + + MachineSpec struct { Serial func(childComplexity int) int Labels func(childComplexity int) int Rack func(childComplexity int) int @@ -61,7 +66,6 @@ type ComplexityRoot struct { RegisterDate func(childComplexity int) int RetireDate func(childComplexity int) int Bmc func(childComplexity int) int - Status func(childComplexity int) int } MachineStatus struct { @@ -80,19 +84,16 @@ type BMCResolver interface { BmcType(ctx context.Context, obj *sabakan.MachineBMC) (string, error) Ipv4(ctx context.Context, obj *sabakan.MachineBMC) (IPAddress, error) } -type MachineResolver interface { - Serial(ctx context.Context, obj *sabakan.Machine) (string, error) - Labels(ctx context.Context, obj *sabakan.Machine) ([]Label, error) - Rack(ctx context.Context, obj *sabakan.Machine) (int, error) - IndexInRack(ctx context.Context, obj *sabakan.Machine) (int, error) - Role(ctx context.Context, obj *sabakan.Machine) (string, error) - Ipv4(ctx context.Context, obj *sabakan.Machine) ([]IPAddress, error) - RegisterDate(ctx context.Context, obj *sabakan.Machine) (DateTime, error) - RetireDate(ctx context.Context, obj *sabakan.Machine) (DateTime, error) - Bmc(ctx context.Context, obj *sabakan.Machine) (sabakan.MachineBMC, error) +type MachineSpecResolver interface { + Labels(ctx context.Context, obj *sabakan.MachineSpec) ([]Label, error) + Rack(ctx context.Context, obj *sabakan.MachineSpec) (int, error) + IndexInRack(ctx context.Context, obj *sabakan.MachineSpec) (int, error) + + Ipv4(ctx context.Context, obj *sabakan.MachineSpec) ([]IPAddress, error) + RegisterDate(ctx context.Context, obj *sabakan.MachineSpec) (DateTime, error) + RetireDate(ctx context.Context, obj *sabakan.MachineSpec) (DateTime, error) } type MachineStatusResolver interface { - State(ctx context.Context, obj *sabakan.MachineStatus) (MachineState, error) Timestamp(ctx context.Context, obj *sabakan.MachineStatus) (DateTime, error) } type QueryResolver interface { @@ -235,75 +236,82 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Label.Value(childComplexity), true - case "Machine.serial": - if e.complexity.Machine.Serial == nil { + case "Machine.spec": + if e.complexity.Machine.Spec == nil { break } - return e.complexity.Machine.Serial(childComplexity), true + return e.complexity.Machine.Spec(childComplexity), true - case "Machine.labels": - if e.complexity.Machine.Labels == nil { + case "Machine.status": + if e.complexity.Machine.Status == nil { break } - return e.complexity.Machine.Labels(childComplexity), true + return e.complexity.Machine.Status(childComplexity), true - case "Machine.rack": - if e.complexity.Machine.Rack == nil { + case "MachineSpec.serial": + if e.complexity.MachineSpec.Serial == nil { break } - return e.complexity.Machine.Rack(childComplexity), true + return e.complexity.MachineSpec.Serial(childComplexity), true - case "Machine.indexInRack": - if e.complexity.Machine.IndexInRack == nil { + case "MachineSpec.labels": + if e.complexity.MachineSpec.Labels == nil { break } - return e.complexity.Machine.IndexInRack(childComplexity), true + return e.complexity.MachineSpec.Labels(childComplexity), true - case "Machine.role": - if e.complexity.Machine.Role == nil { + case "MachineSpec.rack": + if e.complexity.MachineSpec.Rack == nil { break } - return e.complexity.Machine.Role(childComplexity), true + return e.complexity.MachineSpec.Rack(childComplexity), true - case "Machine.ipv4": - if e.complexity.Machine.Ipv4 == nil { + case "MachineSpec.indexInRack": + if e.complexity.MachineSpec.IndexInRack == nil { break } - return e.complexity.Machine.Ipv4(childComplexity), true + return e.complexity.MachineSpec.IndexInRack(childComplexity), true - case "Machine.registerDate": - if e.complexity.Machine.RegisterDate == nil { + case "MachineSpec.role": + if e.complexity.MachineSpec.Role == nil { break } - return e.complexity.Machine.RegisterDate(childComplexity), true + return e.complexity.MachineSpec.Role(childComplexity), true - case "Machine.retireDate": - if e.complexity.Machine.RetireDate == nil { + case "MachineSpec.ipv4": + if e.complexity.MachineSpec.Ipv4 == nil { break } - return e.complexity.Machine.RetireDate(childComplexity), true + return e.complexity.MachineSpec.Ipv4(childComplexity), true - case "Machine.bmc": - if e.complexity.Machine.Bmc == nil { + case "MachineSpec.registerDate": + if e.complexity.MachineSpec.RegisterDate == nil { break } - return e.complexity.Machine.Bmc(childComplexity), true + return e.complexity.MachineSpec.RegisterDate(childComplexity), true - case "Machine.status": - if e.complexity.Machine.Status == nil { + case "MachineSpec.retireDate": + if e.complexity.MachineSpec.RetireDate == nil { break } - return e.complexity.Machine.Status(childComplexity), true + return e.complexity.MachineSpec.RetireDate(childComplexity), true + + case "MachineSpec.bmc": + if e.complexity.MachineSpec.Bmc == nil { + break + } + + return e.complexity.MachineSpec.Bmc(childComplexity), true case "MachineStatus.state": if e.complexity.MachineStatus.State == nil { @@ -560,7 +568,6 @@ var machineImplementors = []string{"Machine"} func (ec *executionContext) _Machine(ctx context.Context, sel ast.SelectionSet, obj *sabakan.Machine) graphql.Marshaler { fields := graphql.CollectFields(ctx, sel, machineImplementors) - var wg sync.WaitGroup out := graphql.NewOrderedMap(len(fields)) invalid := false for i, field := range fields { @@ -569,25 +576,105 @@ func (ec *executionContext) _Machine(ctx context.Context, sel ast.SelectionSet, switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Machine") + case "spec": + out.Values[i] = ec._Machine_spec(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + case "status": + out.Values[i] = ec._Machine_status(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + if invalid { + return graphql.Null + } + return out +} + +// nolint: vetshadow +func (ec *executionContext) _Machine_spec(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { + rctx := &graphql.ResolverContext{ + Object: "Machine", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Spec, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(sabakan.MachineSpec) + rctx.Result = res + + return ec._MachineSpec(ctx, field.Selections, &res) +} + +// nolint: vetshadow +func (ec *executionContext) _Machine_status(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { + rctx := &graphql.ResolverContext{ + Object: "Machine", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Status, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(sabakan.MachineStatus) + rctx.Result = res + + return ec._MachineStatus(ctx, field.Selections, &res) +} + +var machineSpecImplementors = []string{"MachineSpec"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) _MachineSpec(ctx context.Context, sel ast.SelectionSet, obj *sabakan.MachineSpec) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, machineSpecImplementors) + + var wg sync.WaitGroup + out := graphql.NewOrderedMap(len(fields)) + invalid := false + for i, field := range fields { + out.Keys[i] = field.Alias + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MachineSpec") case "serial": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_serial(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalid = true - } - wg.Done() - }(i, field) + out.Values[i] = ec._MachineSpec_serial(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } case "labels": wg.Add(1) go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_labels(ctx, field, obj) + out.Values[i] = ec._MachineSpec_labels(ctx, field, obj) wg.Done() }(i, field) case "rack": wg.Add(1) go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_rack(ctx, field, obj) + out.Values[i] = ec._MachineSpec_rack(ctx, field, obj) if out.Values[i] == graphql.Null { invalid = true } @@ -596,25 +683,21 @@ func (ec *executionContext) _Machine(ctx context.Context, sel ast.SelectionSet, case "indexInRack": wg.Add(1) go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_indexInRack(ctx, field, obj) + out.Values[i] = ec._MachineSpec_indexInRack(ctx, field, obj) if out.Values[i] == graphql.Null { invalid = true } wg.Done() }(i, field) case "role": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_role(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalid = true - } - wg.Done() - }(i, field) + out.Values[i] = ec._MachineSpec_role(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } case "ipv4": wg.Add(1) go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_ipv4(ctx, field, obj) + out.Values[i] = ec._MachineSpec_ipv4(ctx, field, obj) if out.Values[i] == graphql.Null { invalid = true } @@ -623,7 +706,7 @@ func (ec *executionContext) _Machine(ctx context.Context, sel ast.SelectionSet, case "registerDate": wg.Add(1) go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_registerDate(ctx, field, obj) + out.Values[i] = ec._MachineSpec_registerDate(ctx, field, obj) if out.Values[i] == graphql.Null { invalid = true } @@ -632,23 +715,14 @@ func (ec *executionContext) _Machine(ctx context.Context, sel ast.SelectionSet, case "retireDate": wg.Add(1) go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_retireDate(ctx, field, obj) + out.Values[i] = ec._MachineSpec_retireDate(ctx, field, obj) if out.Values[i] == graphql.Null { invalid = true } wg.Done() }(i, field) case "bmc": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._Machine_bmc(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalid = true - } - wg.Done() - }(i, field) - case "status": - out.Values[i] = ec._Machine_status(ctx, field, obj) + out.Values[i] = ec._MachineSpec_bmc(ctx, field, obj) if out.Values[i] == graphql.Null { invalid = true } @@ -664,16 +738,16 @@ func (ec *executionContext) _Machine(ctx context.Context, sel ast.SelectionSet, } // nolint: vetshadow -func (ec *executionContext) _Machine_serial(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_serial(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().Serial(rctx, obj) + return obj.Serial, nil }) if resTmp == nil { if !ec.HasError(rctx) { @@ -687,16 +761,16 @@ func (ec *executionContext) _Machine_serial(ctx context.Context, field graphql.C } // nolint: vetshadow -func (ec *executionContext) _Machine_labels(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_labels(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().Labels(rctx, obj) + return ec.resolvers.MachineSpec().Labels(rctx, obj) }) if resTmp == nil { return graphql.Null @@ -740,16 +814,16 @@ func (ec *executionContext) _Machine_labels(ctx context.Context, field graphql.C } // nolint: vetshadow -func (ec *executionContext) _Machine_rack(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_rack(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().Rack(rctx, obj) + return ec.resolvers.MachineSpec().Rack(rctx, obj) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -763,16 +837,16 @@ func (ec *executionContext) _Machine_rack(ctx context.Context, field graphql.Col } // nolint: vetshadow -func (ec *executionContext) _Machine_indexInRack(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_indexInRack(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().IndexInRack(rctx, obj) + return ec.resolvers.MachineSpec().IndexInRack(rctx, obj) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -786,16 +860,16 @@ func (ec *executionContext) _Machine_indexInRack(ctx context.Context, field grap } // nolint: vetshadow -func (ec *executionContext) _Machine_role(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_role(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().Role(rctx, obj) + return obj.Role, nil }) if resTmp == nil { if !ec.HasError(rctx) { @@ -809,16 +883,16 @@ func (ec *executionContext) _Machine_role(ctx context.Context, field graphql.Col } // nolint: vetshadow -func (ec *executionContext) _Machine_ipv4(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_ipv4(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().Ipv4(rctx, obj) + return ec.resolvers.MachineSpec().Ipv4(rctx, obj) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -841,16 +915,16 @@ func (ec *executionContext) _Machine_ipv4(ctx context.Context, field graphql.Col } // nolint: vetshadow -func (ec *executionContext) _Machine_registerDate(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_registerDate(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().RegisterDate(rctx, obj) + return ec.resolvers.MachineSpec().RegisterDate(rctx, obj) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -864,16 +938,16 @@ func (ec *executionContext) _Machine_registerDate(ctx context.Context, field gra } // nolint: vetshadow -func (ec *executionContext) _Machine_retireDate(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_retireDate(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().RetireDate(rctx, obj) + return ec.resolvers.MachineSpec().RetireDate(rctx, obj) }) if resTmp == nil { if !ec.HasError(rctx) { @@ -887,16 +961,16 @@ func (ec *executionContext) _Machine_retireDate(ctx context.Context, field graph } // nolint: vetshadow -func (ec *executionContext) _Machine_bmc(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { +func (ec *executionContext) _MachineSpec_bmc(ctx context.Context, field graphql.CollectedField, obj *sabakan.MachineSpec) graphql.Marshaler { rctx := &graphql.ResolverContext{ - Object: "Machine", + Object: "MachineSpec", Args: nil, Field: field, } ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Machine().Bmc(rctx, obj) + return obj.BMC, nil }) if resTmp == nil { if !ec.HasError(rctx) { @@ -910,30 +984,6 @@ func (ec *executionContext) _Machine_bmc(ctx context.Context, field graphql.Coll return ec._BMC(ctx, field.Selections, &res) } -// nolint: vetshadow -func (ec *executionContext) _Machine_status(ctx context.Context, field graphql.CollectedField, obj *sabakan.Machine) graphql.Marshaler { - rctx := &graphql.ResolverContext{ - Object: "Machine", - Args: nil, - Field: field, - } - ctx = graphql.WithResolverContext(ctx, rctx) - resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Status, nil - }) - if resTmp == nil { - if !ec.HasError(rctx) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(sabakan.MachineStatus) - rctx.Result = res - - return ec._MachineStatus(ctx, field.Selections, &res) -} - var machineStatusImplementors = []string{"MachineStatus"} // nolint: gocyclo, errcheck, gas, goconst @@ -950,14 +1000,10 @@ func (ec *executionContext) _MachineStatus(ctx context.Context, sel ast.Selectio case "__typename": out.Values[i] = graphql.MarshalString("MachineStatus") case "state": - wg.Add(1) - go func(i int, field graphql.CollectedField) { - out.Values[i] = ec._MachineStatus_state(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalid = true - } - wg.Done() - }(i, field) + out.Values[i] = ec._MachineStatus_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } case "timestamp": wg.Add(1) go func(i int, field graphql.CollectedField) { @@ -993,7 +1039,7 @@ func (ec *executionContext) _MachineStatus_state(ctx context.Context, field grap ctx = graphql.WithResolverContext(ctx, rctx) resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.MachineStatus().State(rctx, obj) + return obj.State, nil }) if resTmp == nil { if !ec.HasError(rctx) { @@ -1001,9 +1047,9 @@ func (ec *executionContext) _MachineStatus_state(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(MachineState) + res := resTmp.(sabakan.MachineState) rctx.Result = res - return res + return MarshalMachineState(res) } // nolint: vetshadow @@ -2652,9 +2698,9 @@ func UnmarshalMachineParams(v interface{}) (MachineParams, error) { rawIf1 = []interface{}{v} } } - it.States = make([]MachineState, len(rawIf1)) + it.States = make([]sabakan.MachineState, len(rawIf1)) for idx1 := range rawIf1 { - err = (&it.States[idx1]).UnmarshalGQL(rawIf1[idx1]) + it.States[idx1], err = UnmarshalMachineState(rawIf1[idx1]) } if err != nil { return it, err @@ -2728,6 +2774,14 @@ input LabelInput { Machine represents a physical server in a datacenter rack. """ type Machine { + spec: MachineSpec! + status: MachineStatus! +} + +""" +MachineSpec represents specifications of a machine. +""" +type MachineSpec { serial: ID! labels: [Label!] rack: Int! @@ -2737,7 +2791,6 @@ type Machine { registerDate: DateTime! retireDate: DateTime! bmc: BMC! - status: MachineStatus! } """ diff --git a/gql/gqlgen.yml b/gql/gqlgen.yml index f278d501..48ea4e98 100644 --- a/gql/gqlgen.yml +++ b/gql/gqlgen.yml @@ -1,7 +1,5 @@ -# .gqlgen.yml example -# # Refer to https://gqlgen.com/config/ -# for detailed .gqlgen.yml documentation. +# for detailed gqlgen.yml documentation. schema: schema.graphql exec: @@ -14,10 +12,14 @@ resolver: models: Machine: model: github.com/cybozu-go/sabakan.Machine + MachineSpec: + model: github.com/cybozu-go/sabakan.MachineSpec BMC: model: github.com/cybozu-go/sabakan.MachineBMC MachineStatus: model: github.com/cybozu-go/sabakan.MachineStatus + MachineState: + model: github.com/cybozu-go/sabakan/gql.MachineState IPAddress: model: github.com/cybozu-go/sabakan/gql.IPAddress DateTime: diff --git a/gql/match.go b/gql/match.go index 0dcae974..9b5387ec 100644 --- a/gql/match.go +++ b/gql/match.go @@ -1,7 +1,6 @@ package gql import ( - "strings" "time" "github.com/cybozu-go/sabakan" @@ -113,7 +112,7 @@ func containsState(h *MachineParams, target sabakan.MachineState, base bool) boo } for _, state := range h.States { - if state.String() == strings.ToUpper(target.String()) { + if state == target { return true } } diff --git a/gql/match_test.go b/gql/match_test.go index cb3ac040..735c7462 100644 --- a/gql/match_test.go +++ b/gql/match_test.go @@ -221,7 +221,7 @@ func TestMatchMachine(t *testing.T) { }, }, having: &MachineParams{ - States: []MachineState{MachineStateUninitialized}, + States: []sabakan.MachineState{sabakan.StateUninitialized}, }, notHaving: &MachineParams{}, now: now, @@ -236,7 +236,7 @@ func TestMatchMachine(t *testing.T) { }, }, having: &MachineParams{ - States: []MachineState{MachineStateUninitialized, MachineStateHealthy}, + States: []sabakan.MachineState{sabakan.StateUninitialized, sabakan.StateHealthy}, }, notHaving: &MachineParams{}, now: now, @@ -252,7 +252,7 @@ func TestMatchMachine(t *testing.T) { }, having: &MachineParams{}, notHaving: &MachineParams{ - States: []MachineState{MachineStateHealthy}, + States: []sabakan.MachineState{sabakan.StateHealthy}, }, now: now, expect: false, diff --git a/gql/models_gen.go b/gql/models_gen.go index 1a1faa41..f41834bb 100644 --- a/gql/models_gen.go +++ b/gql/models_gen.go @@ -3,9 +3,7 @@ package gql import ( - fmt "fmt" - io "io" - strconv "strconv" + sabakan "github.com/cybozu-go/sabakan" ) // Label represents an arbitrary key-value pairs. @@ -22,51 +20,9 @@ type LabelInput struct { // MachineParams is a set of input parameters to search machines. type MachineParams struct { - Labels []LabelInput `json:"labels"` - Racks []int `json:"racks"` - Roles []string `json:"roles"` - States []MachineState `json:"states"` - MinDaysBeforeRetire *int `json:"minDaysBeforeRetire"` -} - -// MachineState enumerates machine states. -type MachineState string - -const ( - MachineStateUninitialized MachineState = "UNINITIALIZED" - MachineStateHealthy MachineState = "HEALTHY" - MachineStateUnhealthy MachineState = "UNHEALTHY" - MachineStateUnreachable MachineState = "UNREACHABLE" - MachineStateUpdating MachineState = "UPDATING" - MachineStateRetiring MachineState = "RETIRING" - MachineStateRetired MachineState = "RETIRED" -) - -func (e MachineState) IsValid() bool { - switch e { - case MachineStateUninitialized, MachineStateHealthy, MachineStateUnhealthy, MachineStateUnreachable, MachineStateUpdating, MachineStateRetiring, MachineStateRetired: - return true - } - return false -} - -func (e MachineState) String() string { - return string(e) -} - -func (e *MachineState) UnmarshalGQL(v interface{}) error { - str, ok := v.(string) - if !ok { - return fmt.Errorf("enums must be strings") - } - - *e = MachineState(str) - if !e.IsValid() { - return fmt.Errorf("%s is not a valid MachineState", str) - } - return nil -} - -func (e MachineState) MarshalGQL(w io.Writer) { - fmt.Fprint(w, strconv.Quote(e.String())) + Labels []LabelInput `json:"labels"` + Racks []int `json:"racks"` + Roles []string `json:"roles"` + States []sabakan.MachineState `json:"states"` + MinDaysBeforeRetire *int `json:"minDaysBeforeRetire"` } diff --git a/gql/resolver_impl.go b/gql/resolver_impl.go index 3a9a7490..92914c1f 100644 --- a/gql/resolver_impl.go +++ b/gql/resolver_impl.go @@ -4,7 +4,6 @@ package gql import ( "context" - "fmt" "net" "sort" "time" @@ -22,9 +21,9 @@ func (r *Resolver) BMC() BMCResolver { return &bMCResolver{r} } -// Machine implements ResolverRoot. -func (r *Resolver) Machine() MachineResolver { - return &machineResolver{r} +// MachineSpec implements ResolverRoot. +func (r *Resolver) MachineSpec() MachineSpecResolver { + return &machineSpecResolver{r} } // MachineStatus implements ResolverRoot. @@ -46,76 +45,47 @@ func (r *bMCResolver) Ipv4(ctx context.Context, obj *sabakan.MachineBMC) (IPAddr return IPAddress(net.ParseIP(obj.IPv4)), nil } -type machineResolver struct{ *Resolver } +type machineSpecResolver struct{ *Resolver } -func (r *machineResolver) Serial(ctx context.Context, obj *sabakan.Machine) (string, error) { - return obj.Spec.Serial, nil -} -func (r *machineResolver) Labels(ctx context.Context, obj *sabakan.Machine) ([]Label, error) { - if len(obj.Spec.Labels) == 0 { +func (r *machineSpecResolver) Labels(ctx context.Context, obj *sabakan.MachineSpec) ([]Label, error) { + if len(obj.Labels) == 0 { return []Label{}, nil } - keys := make([]string, 0, len(obj.Spec.Labels)) - for k := range obj.Spec.Labels { + keys := make([]string, 0, len(obj.Labels)) + for k := range obj.Labels { keys = append(keys, k) } sort.Strings(keys) - labels := make([]Label, 0, len(obj.Spec.Labels)) + labels := make([]Label, 0, len(obj.Labels)) for _, k := range keys { - labels = append(labels, Label{Name: k, Value: obj.Spec.Labels[k]}) + labels = append(labels, Label{Name: k, Value: obj.Labels[k]}) } return labels, nil } -func (r *machineResolver) Rack(ctx context.Context, obj *sabakan.Machine) (int, error) { - return int(obj.Spec.Rack), nil -} -func (r *machineResolver) IndexInRack(ctx context.Context, obj *sabakan.Machine) (int, error) { - return int(obj.Spec.IndexInRack), nil +func (r *machineSpecResolver) Rack(ctx context.Context, obj *sabakan.MachineSpec) (int, error) { + return int(obj.Rack), nil } -func (r *machineResolver) Role(ctx context.Context, obj *sabakan.Machine) (string, error) { - return obj.Spec.Role, nil +func (r *machineSpecResolver) IndexInRack(ctx context.Context, obj *sabakan.MachineSpec) (int, error) { + return int(obj.IndexInRack), nil } -func (r *machineResolver) Ipv4(ctx context.Context, obj *sabakan.Machine) ([]IPAddress, error) { - addresses := make([]IPAddress, len(obj.Spec.IPv4)) - for i, a := range obj.Spec.IPv4 { +func (r *machineSpecResolver) Ipv4(ctx context.Context, obj *sabakan.MachineSpec) ([]IPAddress, error) { + addresses := make([]IPAddress, len(obj.IPv4)) + for i, a := range obj.IPv4 { addresses[i] = IPAddress(net.ParseIP(a)) } return addresses, nil } -func (r *machineResolver) RegisterDate(ctx context.Context, obj *sabakan.Machine) (DateTime, error) { - return DateTime(obj.Spec.RegisterDate), nil -} -func (r *machineResolver) RetireDate(ctx context.Context, obj *sabakan.Machine) (DateTime, error) { - return DateTime(obj.Spec.RetireDate), nil +func (r *machineSpecResolver) RegisterDate(ctx context.Context, obj *sabakan.MachineSpec) (DateTime, error) { + return DateTime(obj.RegisterDate), nil } -func (r *machineResolver) Bmc(ctx context.Context, obj *sabakan.Machine) (sabakan.MachineBMC, error) { - return obj.Spec.BMC, nil +func (r *machineSpecResolver) RetireDate(ctx context.Context, obj *sabakan.MachineSpec) (DateTime, error) { + return DateTime(obj.RetireDate), nil } type machineStatusResolver struct{ *Resolver } -func (r *machineStatusResolver) State(ctx context.Context, obj *sabakan.MachineStatus) (MachineState, error) { - switch obj.State { - case sabakan.StateUninitialized: - return MachineStateUninitialized, nil - case sabakan.StateHealthy: - return MachineStateHealthy, nil - case sabakan.StateUnhealthy: - return MachineStateUnhealthy, nil - case sabakan.StateUnreachable: - return MachineStateUnreachable, nil - case sabakan.StateUpdating: - return MachineStateUpdating, nil - case sabakan.StateRetiring: - return MachineStateRetiring, nil - case sabakan.StateRetired: - return MachineStateRetired, nil - default: - return "", fmt.Errorf("unknown state:%s", obj.State.String()) - } -} func (r *machineStatusResolver) Timestamp(ctx context.Context, obj *sabakan.MachineStatus) (DateTime, error) { return DateTime(obj.Timestamp), nil } diff --git a/gql/schema.graphql b/gql/schema.graphql index c6c7741e..9b340225 100644 --- a/gql/schema.graphql +++ b/gql/schema.graphql @@ -26,6 +26,14 @@ input LabelInput { Machine represents a physical server in a datacenter rack. """ type Machine { + spec: MachineSpec! + status: MachineStatus! +} + +""" +MachineSpec represents specifications of a machine. +""" +type MachineSpec { serial: ID! labels: [Label!] rack: Int! @@ -35,7 +43,6 @@ type Machine { registerDate: DateTime! retireDate: DateTime! bmc: BMC! - status: MachineStatus! } """ diff --git a/gql/types.go b/gql/types.go index f883b6b9..c8a7cc89 100644 --- a/gql/types.go +++ b/gql/types.go @@ -1,12 +1,15 @@ package gql import ( + "errors" "fmt" "io" "net" + "strings" "time" "github.com/99designs/gqlgen/graphql" + "github.com/cybozu-go/sabakan" ) // IPAddress represents "IPAddress" GraphQL custom scalar. @@ -51,3 +54,22 @@ func (dt *DateTime) UnmarshalGQL(v interface{}) error { func (dt DateTime) MarshalGQL(w io.Writer) { graphql.MarshalTime(time.Time(dt)).MarshalGQL(w) } + +// MarshalMachineState helps mapping sabakan.MachineState with GraphQL enum. +func MarshalMachineState(state sabakan.MachineState) graphql.Marshaler { + return graphql.MarshalString(strings.ToUpper(state.String())) +} + +// UnmarshalMachineState helps mapping sabakan.MachineState with GraphQL enum. +func UnmarshalMachineState(v interface{}) (sabakan.MachineState, error) { + str, err := graphql.UnmarshalString(v) + if err != nil { + return "", err + } + st := sabakan.MachineState(strings.ToLower(str)) + if !st.IsValid() { + return "", errors.New("invalid state: " + str) + } + + return st, nil +} diff --git a/machines.go b/machines.go index 946dc814..d81b8308 100644 --- a/machines.go +++ b/machines.go @@ -14,6 +14,28 @@ func (ms MachineState) String() string { return string(ms) } +// IsValid returns true only if the MachineState is pre-defined. +func (ms MachineState) IsValid() bool { + switch ms { + case StateUninitialized: + return true + case StateHealthy: + return true + case StateUnhealthy: + return true + case StateUnreachable: + return true + case StateUpdating: + return true + case StateRetiring: + return true + case StateRetired: + return true + } + + return false +} + // Machine state definitions. const ( StateUninitialized = MachineState("uninitialized") diff --git a/web/machines_test.go b/web/machines_test.go index 15ba4b24..1fa52865 100644 --- a/web/machines_test.go +++ b/web/machines_test.go @@ -393,7 +393,7 @@ func testMachinesGraphQL(t *testing.T) { }) v := url.Values{} - v.Set("query", `{searchMachines {serial}}`) + v.Set("query", `{searchMachines { spec { serial } } }`) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/graphql?"+v.Encode(), nil) handler.ServeHTTP(w, r)