Skip to content

Commit

Permalink
initial redis support
Browse files Browse the repository at this point in the history
  • Loading branch information
danawillow committed May 12, 2018
1 parent 72c987a commit b14a2a2
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 10 deletions.
2 changes: 1 addition & 1 deletion build/terraform
2 changes: 1 addition & 1 deletion coverage/.last_run.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"result": {
"covered_percent": 80.11
"covered_percent": 80.14
}
}
127 changes: 127 additions & 0 deletions products/redis/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Copyright 2018 Google Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

--- !ruby/object:Api::Product
name: Google Cloud Memorystore for Redis
prefix: gredis
versions:
- !ruby/object:Api::Product::Version
name: beta
base_url: https://redis.googleapis.com/v1beta1/
scopes:
# TODO this is fake and made up but MM requires it
- https://www.googleapis.com/auth/redis
objects:
- !ruby/object:Api::Resource
name: 'Instance'
exclude: true
base_url: |
projects/{{project}}/locations/{{region}}/instances?instanceId={{name}}
self_link: projects/{{project}}/locations/{{region}}/instances/{{name}}
description: |
A Google Cloud Redis instance.
input: true
<%= indent(compile_file({}, 'templates/regional_async.yaml.erb'), 4) %>
parameters:
- !ruby/object:Api::Type::String # TODO: resourceref?
name: 'region'
description: |
The name of the Redis region of the instance.
required: true
properties:
- !ruby/object:Api::Type::String
name: alternativeLocationId
description: |
Only applicable to STANDARD_HA tier which protects the instance
against zonal failures by provisioning it across two zones.
If provided, it must be a different zone from the one provided in
[locationId].
- !ruby/object:Api::Type::String
name: authorizedNetwork
description: |
The full name of the Google Compute Engine network to which the
instance is connected. If left unspecified, the default network
will be used.
- !ruby/object:Api::Type::Time
name: createTime
description: |
The time the instance was created in RFC3339 UTC "Zulu" format,
accurate to nanoseconds.
output: true
- !ruby/object:Api::Type::String
name: currentLocationId
description: |
The current zone where the Redis endpoint is placed.
For Basic Tier instances, this will always be the same as the
[locationId] provided by the user at creation time. For Standard Tier
instances, this can be either [locationId] or [alternativeLocationId]
and can change after a failover event.
output: true
- !ruby/object:Api::Type::String
name: displayName
description: |
An arbitrary and optional user-provided name for the instance.
- !ruby/object:Api::Type::String
name: host
description: |
Hostname or IP address of the exposed Redis endpoint used by clients
to connect to the service.
output: true
- !ruby/object:Api::Type::NameValues
name: 'labels'
description: Resource labels to represent user provided metadata.
key_type: Api::Type::String
value_type: Api::Type::String
- !ruby/object:Api::Type::String
name: locationId
description: |
The zone where the instance will be provisioned. If not provided,
the service will choose a zone for the instance. For STANDARD_HA tier,
instances will be created across two zones for protection against
zonal failures. If [alternativeLocationId] is also provided, it must
be different from [locationId].
- !ruby/object:Api::Type::String
name: name
description: |
The ID of the instance or a fully qualified identifier for the instance.
required: true
- !ruby/object:Api::Type::Integer
name: memorySizeGb
description: Redis memory size in GiB.
required: true
- !ruby/object:Api::Type::Integer
name: port
description: The port number of the exposed Redis endpoint.
output: true
- !ruby/object:Api::Type::String
name: redisVersion
description: |
The version of Redis software. If not provided, latest supported
version will be used. Updating the version will perform an
upgrade/downgrade to the new version. Currently, the supported values
are REDIS_3_2 for Redis 3.2.
- !ruby/object:Api::Type::String
name: reservedIpRange
description: |
The CIDR range of internal addresses that are reserved for this
instance. If not provided, the service will choose an unused /29
block, for example, 10.0.0.0/29 or 192.168.0.0/29. Ranges must be
unique and non-overlapping with existing subnets in an authorized
network.
- !ruby/object:Api::Type::Enum
name: tier
description: The service tier of the instance. #TODO describe values
values:
- :BASIC
- :STANDARD_HA
required: true
67 changes: 67 additions & 0 deletions products/redis/terraform.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright 2017 Google Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

--- !ruby/object:Provider::Terraform::Config
overrides: !ruby/object:Provider::ResourceOverrides
Instance: !ruby/object:Provider::Terraform::ResourceOverride
id_format: "{{project}}/{{region}}/{{name}}"
custom_code: !ruby/object:Provider::Terraform::CustomCode
decoder: 'templates/terraform/redis/instance_decoder.erb'
properties:
authorizedNetwork: !ruby/object:Provider::Terraform::PropertyOverride
default: !ruby/object:Provider::Terraform::Default
from_api: true
locationId: !ruby/object:Provider::Terraform::PropertyOverride
default: !ruby/object:Provider::Terraform::Default
from_api: true
name: !ruby/object:Provider::Terraform::PropertyOverride
custom_expand: 'templates/terraform/redis/expand_name.erb'
custom_flatten: 'templates/terraform/redis/flatten_name.erb'
redisVersion: !ruby/object:Provider::Terraform::PropertyOverride
default: !ruby/object:Provider::Terraform::Default
from_api: true
region: !ruby/object:Provider::Terraform::PropertyOverride
required: false
default: !ruby/object:Provider::Terraform::Default
from_api: true
reservedIpRange: !ruby/object:Provider::Terraform::PropertyOverride
default: !ruby/object:Provider::Terraform::Default
from_api: true

# This is for a list of example files.
examples: !ruby/object:Api::Resource::HashArray

# This is for copying files over
files: !ruby/object:Provider::Config::Files
# All of these files will be copied verbatim.
copy:
'google/transport.go': 'templates/terraform/transport.go'
'google/transport_test.go': 'templates/terraform/transport_test.go'
'google/import.go': 'templates/terraform/import.go'
'google/import_test.go': 'templates/terraform/import_test.go'
'google/redis_operation.go': 'templates/terraform/redis_operation.go'
# These files have templating (ERB) code that will be run.
# This is usually to add licensing info, autogeneration notices, etc.
compile:
'google/provider_{{product_name}}_gen.go': 'templates/terraform/provider_gen.erb'

# This is for custom testing code. All of our tests follow a specific pattern
# that sometimes needs to be deviated from. We're working towards a world where
# these handwritten tests would be unnecessary in many cases (custom types).
tests: !ruby/object:Api::Resource::HashArray

# This would be for custom network responses. Tests work by running some block
# of autogenerated code and then verifying the network calls.
# The network call verifications are automatically generated, but can be
# overriden.
test_data: !ruby/object:Provider::Config::TestData
21 changes: 15 additions & 6 deletions provider/terraform/import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,9 @@ module Import
#
# Fields with default values are `project`, `region` and `zone`.
def import_id_formats(resource)
underscored_base_url = resource.base_url
.gsub(/{{[[:word:]]+}}/) do |field_name|
Google::StringUtils.underscore(field_name)
end
self_link_id_format = self_link_id_format(resource)

# TODO: Add support for custom import id
# We assume that all resources have a name field
self_link_id_format = underscored_base_url + '/{{name}}'

# short id: {{project}}/{{zone}}/{{name}}
field_markers = self_link_id_format.scan(/{{[[:word:]]+}}/)
Expand All @@ -50,6 +45,20 @@ def import_id_formats(resource)

[self_link_id_format, short_id_format, short_id_default_format]
end

private

def self_link_id_format(resource)
self_link_url_parts = self_link_raw_url(resource)[1]
self_link_url = if self_link_url_parts.is_a? Array
self_link_url_parts.flatten.join
else
self_link_url_parts
end
self_link_url.gsub(/{{[[:word:]]+}}/) do |field_name|
Google::StringUtils.underscore(field_name)
end
end
end
end
end
13 changes: 13 additions & 0 deletions templates/terraform/redis/expand_name.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) {
project, err := getProject(d, config)
if err != nil {
return nil, err
}

region, err := getRegion(d, config)
if err != nil {
return nil, err
}

return fmt.Sprintf("projects/%s/locations/%s/instances/%s", project, region, v.(string)), nil
}
3 changes: 3 additions & 0 deletions templates/terraform/redis/flatten_name.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
func flatten<%= prefix -%><%= titlelize_property(property) -%>(v interface{}) interface{} {
return GetResourceNameFromSelfLink(v.(string))
}
7 changes: 7 additions & 0 deletions templates/terraform/redis/instance_decoder.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
config := meta.(*Config)
region, err := getRegion(d, config)
if err != nil {
return nil, err
}
res["region"] = region
return res, nil
64 changes: 64 additions & 0 deletions templates/terraform/redis_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package google

import (
"fmt"
"log"
"time"

"github.com/hashicorp/terraform/helper/resource"
"google.golang.org/api/redis/v1beta1"
)

type RedisOperationWaiter struct {
Service *redis.ProjectsLocationsService
Op *redis.Operation
}

func (w *RedisOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
return func() (interface{}, string, error) {
op, err := w.Service.Operations.Get(w.Op.Name).Do()

if err != nil {
return nil, "", err
}

log.Printf("[DEBUG] Got %v while polling for operation %s's 'done' status", op.Done, w.Op.Name)

return op, fmt.Sprint(op.Done), nil
}
}

func (w *RedisOperationWaiter) Conf() *resource.StateChangeConf {
return &resource.StateChangeConf{
Pending: []string{"false"},
Target: []string{"true"},
Refresh: w.RefreshFunc(),
}
}

func redisOperationWait(service *redis.Service, op *redis.Operation, project, activity string) error {
return redisOperationWaitTime(service, op, project, activity, 4)
}

func redisOperationWaitTime(service *redis.Service, op *redis.Operation, project, activity string, timeoutMin int) error {
w := &RedisOperationWaiter{
Service: service.Projects.Locations,
Op: op,
}

state := w.Conf()
state.Delay = 10 * time.Second
state.Timeout = time.Duration(timeoutMin) * time.Minute
state.MinTimeout = 2 * time.Second
opRaw, err := state.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for %s: %s", activity, err)
}

op = opRaw.(*redis.Operation)
if op.Error != nil {
return fmt.Errorf("Error code %v, message: %s", op.Error.Code, op.Error.Message)
}

return nil
}
4 changes: 3 additions & 1 deletion templates/terraform/resource.erb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func resource<%= resource_name -%>Read(d *schema.ResourceData, meta interface{})
return handleNotFoundError(err, d, fmt.Sprintf("<%= resource_name -%> %q", d.Id()))
}
<% if object.custom_code.decoder -%>
res, err = <%= resource_name -%>Decoder(d, meta, res)
res, err = resource<%= resource_name -%>Decoder(d, meta, res)
if err != nil {
return err
}
Expand All @@ -165,9 +165,11 @@ func resource<%= resource_name -%>Read(d *schema.ResourceData, meta interface{})
return fmt.Errorf("Error reading <%= object.name -%>: %s", err)
}
<% end -%>
<% if (object.exports || []).any? { |e| e.is_a?(Api::Type::SelfLink)} -%>
if err := d.Set("self_link", res["selfLink"]); err != nil {
return fmt.Errorf("Error reading <%= object.name -%>: %s", err)
}
<% end -%>
<% if object.base_url.include?("{{project}}") -%>
if err := d.Set("project", project); err != nil {
return fmt.Errorf("Error reading <%= object.name -%>: %s", err)
Expand Down
12 changes: 11 additions & 1 deletion templates/terraform/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"net/http"
urllib "net/url"
"reflect"
"regexp"
"strings"
Expand Down Expand Up @@ -105,7 +106,16 @@ func sendRequest(config *Config, method, url string, body map[string]interface{}
}
}

req, err := http.NewRequest(method, url+"?alt=json", &buf)
u, err := urllib.Parse(url)
if err != nil {
return nil, err
}

queries := u.Query()
queries.Add("alt", "json")
newUrl := u.Scheme + "://" + u.Host + u.Path + "?" + queries.Encode()

req, err := http.NewRequest(method, newUrl, &buf)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit b14a2a2

Please sign in to comment.