diff --git a/README.md b/README.md index 8778a4f..c410f72 100644 --- a/README.md +++ b/README.md @@ -34,43 +34,44 @@ client.range("/service/hello").kvs.try(&.first?) #=> # # #= 0.35" license: MIT + authors: - Caspian Baska - Duke Nguyen diff --git a/spec/kv_spec.cr b/spec/kv_spec.cr index 64833b3..a1aab99 100644 --- a/spec/kv_spec.cr +++ b/spec/kv_spec.cr @@ -54,7 +54,7 @@ module Etcd client = Etcd.from_env response = client.kv.put("#{TEST_PREFIX}/hello", "world") - response.should be_a Model::PutResponse + response.should be_a Model::Put end it "queries a range of keys" do @@ -64,7 +64,7 @@ module Etcd client.kv.put(key, value) response = client.kv.range(key) - response.should be_a Model::RangeResponse + response.should be_a Model::Range values = response.kvs || [] of Model::Kv value_present = values.any? { |r| r.key == key && r.value == value } value_present.should be_true @@ -82,7 +82,7 @@ module Etcd client.kv.put(key1, value1, lease: lease.id) response = client.kv.range_prefix(key0) - response.should be_a Model::RangeResponse + response.should be_a Model::Range values = response.kvs || [] of Model::Kv key_present = values.any? { |r| r.key == key1 && r.value == value1 } key_present.should be_true diff --git a/src/etcd/auth.cr b/src/etcd/auth.cr index ef2d65a..2cb81ad 100644 --- a/src/etcd/auth.cr +++ b/src/etcd/auth.cr @@ -5,82 +5,123 @@ class Etcd::Auth end # auth/authenticate - def authenticate - raise "unimplemented" + def authenticate(name : String, password : String) + validate!(name) + + response = client.api.post("/auth/auth/authenticate", {name: name, password: password}).body + Model::Token.from_json(response).token end # auth/disable def disable - raise "unimplemented" + client.api.post("/auth/auth/disable").success? end # auth/enable def enable - raise "unimplemented" + client.api.post("/auth/auth/enable").success? end # auth/role/add - def role_add - raise "unimplemented" + def role_add(name : String) + validate!(name) + + client.api.post("/auth/role/add", {name: name}).success? end # auth/role/delete - def role_delete - raise "unimplemented" + def role_delete(role : String) + client.api.post("/auth/role/delete", {role: role}).success? end # auth/role/get - def role_get - raise "unimplemented" + def role_get(role : String) + response = client.api.post("/auth/role/get", {role: role}).body + Model::Permissions.from_json(response).perm end # auth/role/grant - def role_grant - raise "unimplemented" + def role_grant(name : String, perm_key : String, range_end : String) + validate!(name) + + options = { + :name => name, + :perm => { + :key => perm_key, + :permType => "READ", + :range_end => range_end, + }, + } + + client.api.post("/auth/role/grant", options).success? end # auth/role/list def role_list - raise "unimplemented" + response = client.api.post("/auth/role/list").body + Roles.from_json(response).roles end # auth/role/revoke - def role_revoke - raise "unimplemented" + def role_revoke(key : String, range_end : String, role : String) + client.api.post("/auth/role/revoke").success? end # auth/user/add - def user_add - raise "unimplemented" + def user_add(name : String, password : String, no_password : Bool) + validate!(name) + + options = { + :name => name, + :options => { + :no_password => no_password, + }, + :password => password, + } + + client.api.post("/auth/user/add", options).success? end # auth/user/changepw - def user_changepw - raise "unimplemented" + def user_changepw(name : String, password : String) + validate!(name) + + client.api.post("/auth/user/changepw", {name: name, password: password}).success? end # auth/user/delete - def user_delete - raise "unimplemented" + def user_delete(name : String) + validate!(name) + + client.api.post("/auth/user/delete", {name: name}).success? end # auth/user/get - def user_get - raise "unimplemented" + def user_get(name : String) + validate!(name) + + response = client.api.post("/auth/user/get", {name: name}).body + Model::Roles.from_json(response).roles end # auth/user/grant - def user_grant - raise "unimplemented" + def user_grant(role : String, user : String) + client.api.post("/auth/user/grant").success? end # auth/user/list def user_list - raise "unimplemented" + response = client.api.post("/auth/user/list").body + Model::Users.from_json(response).users end # auth/user/revoke - def user_revoke - raise "unimplemented" + def user_revoke(name : String, role : String) + validate!(name) + client.api.post("/auth/user/revoke").success? + end + + private def validate!(name : String) + raise ArgumentError.new("Arg name is empty") if name.empty? end end diff --git a/src/etcd/cluster.cr b/src/etcd/cluster.cr index 4a813eb..e63b0c5 100644 --- a/src/etcd/cluster.cr +++ b/src/etcd/cluster.cr @@ -1,26 +1,36 @@ module Etcd::Cluster + private getter client : Etcd::Client + + def initialize(@client = Etcd::Client.new) + end + # POST cluster/member/add - def member_add - raise "unimplemented" + def member_add(is_learner : Bool, peer_urls : Array(String)) + response = client.api.post("/cluster/member/add", {is_learner: is_learner, peerURLs: peer_urls}).body + Model::Cluster::MemberAdd.from_json(response) end # POST cluster/member/list def member_list - raise "unimplemented" + response = client.api.post("/cluster/member/list").body + Model::Cluster::Members.from_json(response).members end # POST cluster/member/promote - def member_promote - raise "unimplemented" + def member_promote(id : UInt64) + response = client.api.post("/cluster/member/promote", {ID: id}).body + Model::Cluster::Members.from_json(response).members end # POST cluster/member/remove - def member_remove - raise "unimplemented" + def member_remove(id : UInt64) + response = client.api.post("/cluster/member/remove", {ID: id}).body + Model::Cluster::Members.from_json(response).members end # POST cluster/member/update - def member_update - raise "unimplemented" + def member_update(id : UInt64, peer_urls : Array(String)) + response = client.api.post("/cluster/member/update", {ID: id, peerURLs: peer_urls}).body + Model::Cluster::Members.from_json(response).members end end diff --git a/src/etcd/kv.cr b/src/etcd/kv.cr index 73bcaf8..335c799 100644 --- a/src/etcd/kv.cr +++ b/src/etcd/kv.cr @@ -38,7 +38,7 @@ module Etcd }.compact response = client.api.post("/kv/put", options) - Model::PutResponse.from_json(response.body) + Model::Put.from_json(response.body) end # Deletes key or range of keys @@ -55,7 +55,7 @@ module Etcd }.compact response = client.api.post("/kv/deleterange", post_body) - Model::DeleteResponse.from_json(response.body) + Model::Delete.from_json(response.body) end # Deletes an entire keyspace prefix @@ -66,7 +66,7 @@ module Etcd end # Queries a range of keys - def range(key, range_end : String? = nil, base64_keys : Bool = true) + def range(key, range_end : String? = nil, limit : Int64 = 0_i64, base64_keys : Bool = true) # Otherwise bypass encoding keys if base64_keys key = Base64.strict_encode(key) @@ -76,17 +76,18 @@ module Etcd post_body = { :key => key, :range_end => range_end, + :limit => limit, }.compact response = client.api.post("/kv/range", post_body) - Model::RangeResponse.from_json(response.body) + Model::Range.from_json(response.body) end # Query keys beneath a prefix - def range_prefix(prefix) + def range_prefix(prefix, limit : Int64 = 0_i64) encoded_prefix = Base64.strict_encode(prefix) range_end = prefix_range_end encoded_prefix - range(encoded_prefix, range_end, base64_keys: false) + range(encoded_prefix, range_end, limit, base64_keys: false) end # Query all keys >= key @@ -96,6 +97,15 @@ module Etcd range(encoded_key, range_end, base64_keys: false) end + def txn(post_body) + response = client.api.post("/kv/txn", post_body) + Model::Txn.from_json(response.body).succeeded + end + + def compaction(physical : Bool, revision : Int64) + client.api.post("/kv/compaction", {:physical => physical, :revision => revision}).success? + end + # Non-Standard Requests ############################################################################## @@ -123,7 +133,7 @@ module Etcd } response = client.api.post("/kv/txn", post_body) - Model::TxnResponse.from_json(response.body).succeeded + Model::Txn.from_json(response.body).succeeded end # Moves a value from `key` to `key_destination`, deleting the kv at `key` in the process. @@ -133,12 +143,20 @@ module Etcd value = Base64.strict_encode(value.to_s) post_body = { - :compare => [{ - :key => key_d, - :value => Base64.strict_encode("0"), - :target => "VERSION", - :result => "EQUAL", - }], + :compare => [ + { + :key => key_d, + :value => Base64.strict_encode("0"), + :target => "VERSION", + :result => "EQUAL", + }, + { + :key => key_o, + :value => Base64.strict_encode("0"), + :target => "VERSION", + :result => "NOT_EQUAL", + }, + ], :success => [ { :request_put => { @@ -186,7 +204,7 @@ module Etcd }], } - Model::TxnResponse.from_json(client.api.post("/kv/txn", post_body).body).succeeded + Model::Txn.from_json(client.api.post("/kv/txn", post_body).body).succeeded end def get(key) : String? diff --git a/src/etcd/lease.cr b/src/etcd/lease.cr index 1673b48..4be0908 100644 --- a/src/etcd/lease.cr +++ b/src/etcd/lease.cr @@ -6,11 +6,11 @@ class Etcd::Lease def initialize(@client = Etcd::Client.new) end - # Requests a lease - # ttl ttl of granted lease Int64 - # id id of 0 prompts etcd to assign any id to lease UInt64 - def grant(ttl : Int64 = @ttl, id = 0) - Model::Grant.from_json(client.api.post("/lease/grant", {TTL: ttl, ID: 0}).body) + # /kv/lease/leases + # /lease/leases + # Queries for all existing leases in an etcd cluster + def leases + Model::Leases.from_json(client.api.post("/kv/lease/leases").body).leases.map(&.id) end # Requests persistence of lease. @@ -21,6 +21,16 @@ class Etcd::Lease nil end + # /kv/lease/revoke + # Revokes an etcd lease + # id Id of lease Int64 + def revoke(id : Int64) + # To get header: Etcd::Model::WithHeader.from_json(response.body) + client.api.post("/kv/lease/revoke", {ID: id}).success? + end + + # /kv/lease/timetolive + # /lease/timetolive # Queries the TTL of a lease # id id of lease Int64 # query_keys query all the lease's keys for ttl Bool @@ -28,14 +38,20 @@ class Etcd::Lease Model::TimeToLive.from_json(client.api.post("/kv/lease/timetolive", {ID: id, keys: query_keys}).body) end - # Revokes an etcd lease - # id Id of lease Int64 - def revoke(id : Int64) - client.api.post("/kv/lease/revoke", {ID: id}).success? + # /lease/grant + # Requests a lease + # ttl ttl of granted lease Int64 + # id id of 0 prompts etcd to assign any id to lease UInt64 + def grant(ttl : Int64 = @ttl, id = 0) + Model::Grant.from_json(client.api.post("/lease/grant", {TTL: ttl, ID: 0}).body) end - # Queries for all existing leases in an etcd cluster - def leases - Model::LeasesArray.from_json(client.api.post("/kv/lease/leases").body).leases.map(&.id) + # /lease/keepalive + # Requests persistence of lease. + # Must be invoked periodically to avoid key loss. + def keep_alive(id : Int64) : Int64? + model = Model::KeepAlive.from_json(client.api.post("/lease/keepalive", {ID: id}).body) + raise Exception.new(model.error.not_nil!.to_s) if model.result.nil? + model.result.not_nil!.ttl end end diff --git a/src/etcd/maintenance.cr b/src/etcd/maintenance.cr index d3a554f..1e46f5b 100644 --- a/src/etcd/maintenance.cr +++ b/src/etcd/maintenance.cr @@ -6,6 +6,26 @@ class Etcd::Maintenance def initialize(@client = Etcd::Client.new) end + def alarm(action : Model::AlarmAction, alarm : Model::AlarmType, member_id : UInt64) + response = client.api.post("/maintenance/alarm", {action: action, alarm: alarm, memberID: member_id}).body + Model::Alarms.from_json(response).alarms + end + + def defragment + client.api.post("/maintenance/defragment").success? + end + + def hash(revision : String) + response = client.api.post("/maintenance/hash").body + Model::Revision.from_json(response) + end + + def snapshot + model = Model::Snapshot.from_json(client.api.post("/maintenance/snapshot").body) + raise Exception.new(model.error.not_nil!.to_s) if model.result.nil? + model.result + end + # Queries status of etcd instance def status Model::Status.from_json(client.api.post("/maintenance/status").body) @@ -15,4 +35,8 @@ class Etcd::Maintenance def leader status.leader end + + def transfer_leadership(target_id : UInt64) + client.api.post("/maintenance/transfer_leadership", {targetID: target_id}).success? + end end diff --git a/src/etcd/model/auth.cr b/src/etcd/model/auth.cr index e69de29..f096935 100644 --- a/src/etcd/model/auth.cr +++ b/src/etcd/model/auth.cr @@ -0,0 +1,32 @@ +require "./base" + +module Etcd::Model + struct Token < WithHeader + getter token : String + end + + struct Permissions < WithHeader + getter perm : Array(Permission) + end + + enum PermissionType + READ + WRITE + READWRITE + end + + struct Permission < WithHeader + getter key : String # Bytes + @[JSON::Field(key: "permType")] + getter perm_type : PermissionType + getter range_end : String # Bytes + end + + struct Roles < WithHeader + getter roles : Array(String) + end + + struct Users < WithHeader + getter user : Array(String) + end +end diff --git a/src/etcd/model/base.cr b/src/etcd/model/base.cr index 7b18751..1212102 100644 --- a/src/etcd/model/base.cr +++ b/src/etcd/model/base.cr @@ -3,10 +3,33 @@ require "json" # Etcd data models # Refer to documentation https://coreos.com/etcd/docs/latest/dev-guide/api_reference_v3.html module Etcd::Model + struct Enum + def to_json(json : JSON::Builder) + json.string(value) + end + end + private abstract struct Base include JSON::Serializable end + abstract struct WithHeader < Base + getter header : Header + end + + struct Error < Base + getter details : Array(ErrorDetail) + getter grpc_code : Int32 + getter http_code : Int32 + getter http_status : String + getter message : String + end + + struct ErrorDetail < Base + getter type_url : String + getter value : String # Bytes + end + struct Header < Base @[JSON::Field(converter: Etcd::Model::StringTypeConverter(UInt64))] getter cluster_id : UInt64? diff --git a/src/etcd/model/cluster.cr b/src/etcd/model/cluster.cr index e69de29..f971fac 100644 --- a/src/etcd/model/cluster.cr +++ b/src/etcd/model/cluster.cr @@ -0,0 +1,24 @@ +require "./base" + +module Etcd::Model + struct MemberAdd < WithHeader + getter member : Member + getter members : Array(Member) + end + + struct Members < WithHeader + getter members : Array(Member) + end + + struct Member + @[JSON::Field(key: "ID", converter: Etcd::Model::StringTypeConverter(UInt64))] + getter id : UInt64 + @[JSON::Field(key: "clientURLs")] + getter client_urls : Array(String) + @[JSON::Field(key: "isLearner")] + getter is_learner : Bool + getter name : String + @[JSON::Field(key: "peerURLs")] + getter peer_urls : Array(String) + end +end diff --git a/src/etcd/model/kv.cr b/src/etcd/model/kv.cr index f15ba84..85c25e2 100644 --- a/src/etcd/model/kv.cr +++ b/src/etcd/model/kv.cr @@ -2,48 +2,45 @@ require "./base" module Etcd::Model struct Kv < Base - @[JSON::Field(converter: Etcd::Model::Base64Converter)] - getter key : String - @[JSON::Field(converter: Etcd::Model::Base64Converter)] - getter value : String? @[JSON::Field(converter: Etcd::Model::StringTypeConverter(UInt64))] getter create_revision : UInt64? + @[JSON::Field(converter: Etcd::Model::Base64Converter)] + getter key : String + @[JSON::Field(converter: Etcd::Model::StringTypeConverter(Int64))] + getter lease : Int64? @[JSON::Field(converter: Etcd::Model::StringTypeConverter(UInt64))] getter mod_revision : UInt64? + @[JSON::Field(converter: Etcd::Model::Base64Converter)] + getter value : String? @[JSON::Field(converter: Etcd::Model::StringTypeConverter(Int64))] getter version : Int64? - @[JSON::Field(converter: Etcd::Model::StringTypeConverter(Int64))] - getter lease : Int64? end - struct RangeResponse < Base + struct Range < Base getter header : Header? @[JSON::Field(converter: Etcd::Model::StringTypeConverter(Int32))] getter count : Int32 = 0 getter kvs : Array(Etcd::Model::Kv) = [] of Etcd::Model::Kv end - struct PutResponse < Base - getter header : Header + struct Put < WithHeader getter prev_kv : Kv? end - struct DeleteResponse < Base - getter header : Header + struct Delete < WithHeader @[JSON::Field(converter: Etcd::Model::StringTypeConverter(Int32))] getter deleted : Int32 = 0 getter prev_kvs : Array(Etcd::Model::Kv) = [] of Etcd::Model::Kv end struct TxnResponse < Base - getter header : Header - getter succeeded : Bool = false + getter response_range : Range? + getter response_put : Put? + getter response_delete : Delete? + end - alias Response = NamedTuple( - response_range: Etcd::Model::RangeResponse?, - response_put: Etcd::Model::PutResponse?, - response_delete: Etcd::Model::DeleteResponse?, - ) - getter responses : Array(Etcd::Model::TxnResponse::Response) = [] of Etcd::Model::TxnResponse::Response + struct Txn < WithHeader + getter succeeded : Bool = false + getter responses : Array(TxnResponse) = [] of TxnResponse end end diff --git a/src/etcd/model/lease.cr b/src/etcd/model/lease.cr index 896ead0..f0a4a29 100644 --- a/src/etcd/model/lease.cr +++ b/src/etcd/model/lease.cr @@ -1,31 +1,44 @@ require "./base" module Etcd::Model - struct Grant < Base - @[JSON::Field(key: "ID", converter: Etcd::Model::StringTypeConverter(Int64))] - getter id : Int64 - @[JSON::Field(key: "TTL", converter: Etcd::Model::StringTypeConverter(Int64))] - getter ttl : Int64 + struct Leases < WithHeader + getter leases : Array(Lease) end - struct KeepAlive < Base - @[JSON::Field(root: "TTL", converter: Etcd::Model::StringTypeConverter(Int64))] - getter result : Int64? + struct Lease < Base + @[JSON::Field(key: "ID", converter: Etcd::Model::StringTypeConverter(Int64))] + getter id : Int64 end - struct TimeToLive < Base + struct TimeToLive < WithHeader + @[JSON::Field(key: "ID", converter: Etcd::Model::StringTypeConverter(Int64))] + getter id : Int64 + @[JSON::Field(key: "TTL", converter: Etcd::Model::StringTypeConverter(Int64))] + getter ttl : Int64 @[JSON::Field(key: "grantedTTL", converter: Etcd::Model::StringTypeConverter(Int64))] getter granted_ttl : Int64 + getter keys : Array(String)? # This should be Array(Bytes)? + end + + # Returns error + struct Grant < WithHeader + @[JSON::Field(key: "ID", converter: Etcd::Model::StringTypeConverter(Int64))] + getter id : Int64 @[JSON::Field(key: "TTL", converter: Etcd::Model::StringTypeConverter(Int64))] getter ttl : Int64 + getter error : String? end - struct LeasesArray < Base - getter leases : Array(LeasesItem) + # Returns error + struct KeepAlive < Base + getter error : Error? + getter result : KeepAliveResult? end - struct LeasesItem < Base + struct KeepAliveResult < WithHeader @[JSON::Field(key: "ID", converter: Etcd::Model::StringTypeConverter(Int64))] getter id : Int64 + @[JSON::Field(key: "TTL", converter: Etcd::Model::StringTypeConverter(Int64))] + getter ttl : Int64 end end diff --git a/src/etcd/model/maintenance.cr b/src/etcd/model/maintenance.cr index 5593a26..0a8f941 100644 --- a/src/etcd/model/maintenance.cr +++ b/src/etcd/model/maintenance.cr @@ -1,16 +1,62 @@ require "./base" module Etcd::Model - struct Status < Base - getter header : Header - getter version : String + enum AlarmAction + GET + ACTIVATE + DEACTIVATE + end + + enum AlarmType + NONE + NOSPACE + CORRUPT + end + + struct Alarm < Base + getter alarm : AlarmType + @[JSON::Field(converter: Etcd::Model::StringTypeConverter(UInt64))] + getter member_id : UInt64 + end + + struct Alarms < WithHeader + getter alarms : Array(Alarm) + end + + struct Revision < WithHeader + @[JSON::Field(converter: Etcd::Model::StringTypeConverter(Int64))] + getter compact_revision : Int64 + @[JSON::Field(converter: Etcd::Model::StringTypeConverter(Int64))] + getter hash : Int64 + end + + struct Snapshot < Base + getter error : Error? + getter result : SnapshotResult? + end + + struct SnapshotResult < WithHeader + getter blob : String # Bytes + @[JSON::Field(converter: Etcd::Model::StringTypeConverter(UInt64))] + getter remaining_bytes : UInt64 + end + + struct Status < WithHeader @[JSON::Field(key: "dbSize", converter: Etcd::Model::StringTypeConverter(Int64))] getter db_size : Int64 + @[JSON::Field(key: "dbSizeInUse", converter: Etcd::Model::StringTypeConverter(Int64))] + getter db_size_in_use : Int64 + getter errors : Array(String)? + @[JSON::Field(key: "isLearner")] + getter is_learner : Bool? @[JSON::Field(converter: Etcd::Model::StringTypeConverter(UInt64))] getter leader : UInt64 + @[JSON::Field(key: "raftAppliedIndex", converter: Etcd::Model::StringTypeConverter(UInt64))] + getter raft_applied_index : UInt64 @[JSON::Field(key: "raftIndex", converter: Etcd::Model::StringTypeConverter(UInt64))] getter raft_index : UInt64 @[JSON::Field(key: "raftTerm", converter: Etcd::Model::StringTypeConverter(UInt64))] getter raft_term : UInt64 + getter version : String end end diff --git a/src/etcd/watch.cr b/src/etcd/watch.cr index dac12f5..64811d6 100644 --- a/src/etcd/watch.cr +++ b/src/etcd/watch.cr @@ -71,7 +71,7 @@ class Etcd::Watch Watcher.new( key: key, - create_api: ->{ client.spawn_api }, + create_api: ->@client.spawn_api, range_end: range_end, filters: filters, start_revision: start_revision,