Simplify and test Percona::WebAPI::Client.pm according to new web API specs.

This commit is contained in:
Daniel Nichter
2013-01-31 12:58:09 -07:00
parent a8da9c268a
commit 24e505a43d
9 changed files with 332 additions and 99 deletions

View File

@@ -4649,7 +4649,6 @@ sub init_agent {
_info('Initializing agent');
# Do a version-check every time the agent starts. If versions
# have changed, this can affect how services are implemented.
$versions ||= get_versions();
@@ -4698,7 +4697,7 @@ sub init_agent {
# ################################ #
# Run the agent, i.e. exec the main loop to check/update the config
# and services. Doesn't return until service stopped or killed.
# and services. Doesn't return until the service is stopped or killed.
sub run_agent {
my (%args) = @_;

View File

@@ -20,11 +20,14 @@
{
package Percona::Test::Mock::UserAgent;
use Percona::Toolkit qw(Dumper);
sub new {
my ($class, %args) = @_;
my $self = {
encode => $args{encode} || sub { return $_[0] },
decode => $args{decode} || sub { return $_[0] },
requests => [],
responses => {
get => [],
post => [],
@@ -41,11 +44,12 @@ sub new {
sub request {
my ($self, $req) = @_;
my $type = lc($req->method);
push @{$self->{requests}}, uc($type) . ' ' . $req->uri;
if ( $type eq 'post' || $type eq 'put' ) {
push @{$self->{content}->{$type}}, $req->content;
}
my $r = shift @{$self->{responses}->{$type}};
my $c = $self->{encode}->($r->{content});
my $c = $r->{content} ? $self->{encode}->($r->{content}) : '';
my $h = HTTP::Headers->new;
$h->header(%{$r->{headers}}) if exists $r->{headers};
my $res = HTTP::Response->new(

View File

@@ -1,5 +1,4 @@
# This program is copyright 2012-2013 Percona Inc.
# Feedback and improvements are welcome.
# This program is copyright 2012 codenode LLC, 2012-2013 Percona Ireland Ltd.
#
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
@@ -46,20 +45,13 @@ has 'api_key' => (
required => 1,
);
has 'base_url' => (
has 'entry_link' => (
is => 'rw',
isa => 'Str',
default => sub { return 'https://api.tools.percona.com' },
required => 0,
);
has 'links' => (
is => 'rw',
isa => 'HashRef',
lazy => 1,
default => sub { return +{} },
);
has 'ua' => (
is => 'rw',
isa => 'Object',
@@ -79,48 +71,24 @@ sub _build_ua {
my $self = shift;
my $ua = LWP::UserAgent->new;
$ua->agent("Percona::WebAPI::Client/$Percona::WebAPI::Client::VERSION");
$ua->default_header('application/json');
$ua->default_header('Content-Type', 'application/json');
$ua->default_header('X-Percona-API-Key', $self->api_key);
return $ua;
}
sub BUILD {
my ($self) = @_;
eval {
$self->get(
url => $self->base_url,
);
};
if ( my $e = $EVAL_ERROR ) {
if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) {
die $e;
}
else {
die "Unknown error: $e";
}
}
return;
}
sub get {
my ($self, %args) = @_;
have_required_args(\%args, qw(
url
link
)) or die;
my ($url) = $args{url};
my ($link) = $args{link};
# Returns:
my @resources;
# Get resource representations from the url. The server should always
# return a list of resource reps, even if there's only one resource.
# Get the resources at the link.
eval {
$self->_request(
method => 'GET',
url => $url,
link => $link,
);
};
if ( my $e = $EVAL_ERROR ) {
@@ -132,10 +100,8 @@ sub get {
}
}
# Transform the resource representations into an arrayref of hashrefs.
# Each hashref contains (hopefully) all the attribs necessary to create
# a corresponding resource object.
my $res = eval {
# The resource should be represented as JSON, decode it.
my $resource = eval {
decode_json($self->response->content);
};
if ( $EVAL_ERROR ) {
@@ -145,22 +111,26 @@ sub get {
return;
}
my $objs;
# If the server tells us the resource's type, create a new object
# of that type. Else, if there's no type, there's no resource, so
# we should have received links. This usually only happens for the
# entry link. The returned resource objects ref may be scalar or
# an arrayref; the caller should know.
my $resource_objects;
if ( my $type = $self->response->headers->{'x-percona-resource-type'} ) {
eval {
my $type = "Percona::WebAPI::Resource::$type";
# Create resource objects using the server-provided attribs.
if ( ref $res eq 'ARRAY' ) {
$type = "Percona::WebAPI::Resource::$type";
if ( ref $resource eq 'ARRAY' ) {
PTDEBUG && _d('Got a list of', $type, 'resources');
foreach my $attribs ( @$res ) {
$resource_objects = [];
foreach my $attribs ( @$resource ) {
my $obj = $type->new(%$attribs);
push @$objs, $obj;
push @$resource_objects, $obj;
}
}
else {
PTDEBUG && _d('Got a', $type, 'resource');
$objs = $type->new(%$res);
PTDEBUG && _d('Got a', $type, 'resource', Dumper($resource));
$resource_objects = $type->new(%$resource);
}
};
if ( $EVAL_ERROR ) {
@@ -168,44 +138,54 @@ sub get {
return;
}
}
elsif ( $res ) {
$self->update_links($res);
elsif ( exists $resource->{links} ) {
# Lie to the caller: this isn't an object, but the caller can
# treat it like one, e.g. my $links = $api->get(<entry links>);
# then access $links->{self}. A Links object couldn't have
# dynamic attribs anyway, so no use having a real Links obj.
$resource_objects = $resource->{links};
}
else {
warn "Did not get X-Percona-Resource-Type or content from $url\n";
warn "Did not get X-Percona-Resource-Type or links from $link\n";
}
return $objs;
return $resource_objects;
}
# For a successful POST, the server sets the Location header with
# the URI of the newly created resource.
sub post {
my $self = shift;
return $self->_set(
$self->_set(
@_,
method => 'POST',
);
return $self->response->header('Location');
}
# For a successful PUT, the server returns nothing because the caller
# already has the resources URI (if not, the caller should POST).
sub put {
my $self = shift;
return $self->_set(
$self->_set(
@_,
method => 'PUT',
);
return;
}
sub delete {
my ($self, %args) = @_;
have_required_args(\%args, qw(
url
link
)) or die;
my ($url) = $args{url};
my ($link) = $args{link};
eval {
$self->_request(
method => 'DELETE',
url => $url,
link => $link,
headers => { 'Content-Length' => 0 },
);
};
@@ -221,17 +201,18 @@ sub delete {
return;
}
# Low-level POST and PUT handler.
sub _set {
my ($self, %args) = @_;
have_required_args(\%args, qw(
method
resources
url
link
)) or die;
my $method = $args{method};
my $res = $args{resources};
my $url = $args{url};
my $link = $args{link};
my $content = '';
if ( ref($res) eq 'ARRAY' ) {
@@ -262,7 +243,7 @@ sub _set {
eval {
$self->_request(
method => $method,
url => $url,
link => $link,
content => $content,
);
};
@@ -275,30 +256,21 @@ sub _set {
}
}
my $response = eval {
decode_json($self->response->content);
};
if ( $EVAL_ERROR ) {
warn sprintf "Error decoding response to $method $url: %s: %s",
$self->response->content,
$EVAL_ERROR;
return;
}
$self->update_links($response);
return;
}
# Low-level HTTP request handler for all methods. Sets $self->response
# from the request. Returns nothing on success (HTTP status 2xx-3xx),
# else throws an Percona::WebAPI::Exception::Request.
sub _request {
my ($self, %args) = @_;
have_required_args(\%args, qw(
method
url
link
)) or die;
my $method = $args{method};
my $url = $args{url};
my $link = $args{link};
my @optional_args = (
'content',
@@ -306,12 +278,12 @@ sub _request {
);
my ($content, $headers) = @args{@optional_args};
my $req = HTTP::Request->new($method => $url);
my $req = HTTP::Request->new($method => $link);
$req->content($content) if $content;
if ( uc($method) eq 'DELETE' ) {
$self->ua->default_header('Content-Length' => 0);
}
PTDEBUG && _d('Request', $method, $url, Dumper($req));
PTDEBUG && _d('Request', $method, $link, Dumper($req));
my $response = $self->ua->request($req);
PTDEBUG && _d('Response', Dumper($response));
@@ -323,10 +295,10 @@ sub _request {
if ( !($response->code >= 200 && $response->code < 400) ) {
die Percona::WebAPI::Exception::Request->new(
method => $method,
url => $url,
link => $link,
content => $content,
status => $response->code,
error => "Failed to $method $url"
error => "Failed to $method $link",
);
}
@@ -335,16 +307,6 @@ sub _request {
return;
}
sub update_links {
my ($self, $links) = @_;
return unless $links && ref $links && scalar keys %$links;
while (my ($rel, $link) = each %$links) {
$self->links->{$rel} = $link;
}
PTDEBUG && _d('Updated links', Dumper($self->links));
return;
}
no Lmo;
1;
}

View File

@@ -31,14 +31,17 @@ our @EXPORT_OK = qw(
);
sub as_hashref {
my $resource = shift;
my ($resource, %args) = @_;
# Copy the object into a new hashref.
my $as_hashref = { %$resource };
# Delete the links because they're just for client-side use
# and the caller should be sending this object, not getting it.
delete $as_hashref->{links};
# But sometimes for testing we want to keep the links.
if ( !defined $args{with_links} || !$args{with_links} ) {
delete $as_hashref->{links};
}
return $as_hashref;
}

View File

@@ -38,7 +38,13 @@ has 'versions' => (
is => 'ro',
isa => 'Maybe[HashRef]',
required => 0,
default => undef,
);
has 'links' => (
is => 'rw',
isa => 'Maybe[HashRef]',
required => 0,
default => sub { return {} },
);
no Lmo;

View File

@@ -22,12 +22,31 @@ package Percona::WebAPI::Resource::Config;
use Lmo;
has 'id' => (
is => 'r0',
isa => 'Int',
required => 1,
);
has 'name' => (
is => 'ro',
isa => 'Str',
required => 1,
);
has 'options' => (
is => 'ro',
isa => 'HashRef',
required => 1,
);
has 'links' => (
is => 'rw',
isa => 'Maybe[HashRef]',
required => 0,
default => sub { return {} },
);
no Lmo;
1;
}

View File

@@ -44,7 +44,6 @@ has 'query' => (
is => 'ro',
isa => 'Maybe[Str]',
required => 0,
default => undef,
);
has 'output' => (

View File

@@ -46,6 +46,13 @@ has 'spool_schedule' => (
required => 0,
);
has 'links' => (
is => 'rw',
isa => 'Maybe[HashRef]',
required => 0,
default => sub { return {} },
);
sub BUILDARGS {
my ($class, %args) = @_;
if ( ref $args{runs} eq 'ARRAY' ) {

View File

@@ -0,0 +1,234 @@
#!/usr/bin/env perl
BEGIN {
die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
};
use strict;
use warnings FATAL => 'all';
use English qw(-no_match_vars);
use Test::More;
use JSON;
use File::Temp qw(tempdir);
use Percona::Test;
use Percona::Test::Mock::UserAgent;
use Percona::WebAPI::Client;
use Percona::WebAPI::Resource::Agent;
use Percona::WebAPI::Resource::Config;
use Percona::WebAPI::Resource::Service;
use Percona::WebAPI::Resource::Run;
Percona::Toolkit->import(qw(Dumper have_required_args));
Percona::WebAPI::Representation->import(qw(as_json as_hashref));
# #############################################################################
# Create a client with a mock user-agent.
# #############################################################################
my $json = JSON->new;
$json->allow_blessed([]);
$json->convert_blessed([]);
my $ua = Percona::Test::Mock::UserAgent->new(
encode => sub { my $c = shift; return $json->encode($c || {}) },
);
my $client = eval {
Percona::WebAPI::Client->new(
api_key => '123',
ua => $ua,
);
};
is(
$EVAL_ERROR,
'',
'Create client'
) or die;
# #############################################################################
# First thing a client should do is get the entry links.
# #############################################################################
my $return_links = { # what the server returns
agents => '/agents',
};
$ua->{responses}->{get} = [
{
content => {
links => $return_links,
}
},
];
my $links = $client->get(link => $client->entry_link);
is_deeply(
$links,
$return_links,
"Get entry links"
) or diag(Dumper($links));
is_deeply(
$ua->{requests},
[
'GET https://api.tools.percona.com',
],
"1 request, 1 GET"
) or diag(Dumper($ua->{requests}));
# #############################################################################
# Second, a new client will POST an Agent for itself. The entry links
# should have an "agents" link. The server response is empty but the
# URI for the new Agent resource is given by the Location header.
# #############################################################################
my $agent = Percona::WebAPI::Resource::Agent->new(
id => '123',
hostname => 'host',
);
$ua->{responses}->{post} = [
{
headers => { 'Location' => 'agents/5' },
content => '',
},
];
my $uri = $client->post(resources => $agent, link => $links->{agents});
is(
$uri,
"agents/5",
"POST Agent, got Location URI"
);
# #############################################################################
# After successfully creating the new Agent, the client should fetch
# the new Agent resoruce which will have links to the next step: the
# agent's config.
# #############################################################################
$return_links = {
self => 'agents/5',
config => 'agents/5/config',
};
my $content = {
%{ as_hashref($agent) },
links => $return_links,
};
$ua->{responses}->{get} = [
{
headers => { 'X-Percona-Resource-Type' => 'Agent' },
content => $content,
},
];
# Re-using $agent, i.e. updating it with the actual, newly created
# Agent resource as returned by the server with links.
$agent = $client->get(link => $uri);
# Need to use with_links=>1 here because by as_hashref() removes
# links by default because it's usually used to encode and send
# resources, and clients never send links; but here we're using
# it for testing.
is_deeply(
as_hashref($agent, with_links => 1),
$content,
"GET Agent with links"
) or diag(Dumper(as_hashref($agent, with_links => 1)));
# #############################################################################
# Now the agent can get its Config.
# #############################################################################
$return_links = {
self => 'agents/5/config',
services => 'agents/5/services',
};
my $return_config = Percona::WebAPI::Resource::Config->new(
id => '1',
name => 'Default',
options => {},
links => $return_links,
);
$ua->{responses}->{get} = [
{
headers => { 'X-Percona-Resource-Type' => 'Config' },
content => as_hashref($return_config, with_links => 1),
},
];
my $config = $client->get(link => $agent->links->{config});
is_deeply(
as_hashref($config, with_links => 1),
as_hashref($return_config, with_links => 1),
"GET Config"
) or diag(Dumper(as_hashref($config, with_links => 1)));
# #############################################################################
# Once an agent is configured, i.e. successfully gets a Config resource,
# its Config should have a services link which returns a list of Service
# resources, each with their own links.
# #############################################################################
$return_links = {
'send_data' => '/query-monitor',
};
my $run0 = Percona::WebAPI::Resource::Run->new(
number => '0',
program => 'pt-query-digest',
options => '--output json',
output => 'spool',
);
my $svc0 = Percona::WebAPI::Resource::Service->new(
name => 'query-monitor',
run_schedule => '1 * * * *',
spool_schedule => '2 * * * *',
runs => [ $run0 ],
links => $return_links,
);
$ua->{responses}->{get} = [
{
headers => { 'X-Percona-Resource-Type' => 'Service' },
content => [ as_hashref($svc0, with_links => 1) ],
},
];
my $services = $client->get(link => $config->links->{services});
is(
scalar @$services,
1,
"Got 1 service"
);
is_deeply(
as_hashref($services->[0], with_links => 1),
as_hashref($svc0, with_links => 1),
"GET Services"
) or diag(Dumper(as_hashref($services, with_links => 1)));
is(
$services->[0]->links->{send_data},
"/query-monitor",
"send_data link for Service"
);
# #############################################################################
# Done.
# #############################################################################
done_testing;