diff --git a/Gopkg.lock b/Gopkg.lock index e7ea940..5503c24 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -17,6 +17,12 @@ revision = "4b6ea7319e214d98c938f12692336f7ca9348d6b" version = "v0.10.0" +[[projects]] + branch = "master" + name = "github.com/allan-simon/go-singleinstance" + packages = ["."] + revision = "79edcfdc2dfc93da913f46ae8d9f8a9602250431" + [[projects]] name = "github.com/asaskevich/govalidator" packages = ["."] @@ -91,6 +97,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "92b7325f6a94cda806d124a617bcc6703c943d68c51e2be3182ef4ed42dc35a7" + inputs-digest = "110d97437b0209f38ce5b41683f4bdb8842f608d32b27d184ddc13edb6bfd8c9" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 62d3944..99977a0 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -55,3 +55,7 @@ [[constraint]] name = "github.com/pmezard/go-difflib" revision = "792786c7400a136282c1664665ae0a8db921c6c2" + +[[constraint]] + branch = "master" + name = "github.com/allan-simon/go-singleinstance" diff --git a/README.md b/README.md index f652d40..a10ee2b 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ Once your account has been provisioned, we recommend you to follow the configura ## Manual Setup -Use IMCO's Web UI to navigate the menus to `Settings` > `User Details` and scroll down until you find the `New API Key` button. +Use IMCO's Web UI to navigate the menus to `Settings` > `User Details` and scroll down until you find the `API Key` button. API Key -Pressing `New API Key` will download a compressed file that contains the necessary files to authenticate with IMCO API and manage your infrastructure. `Keep it safe`. +Pressing `Create and download a new API key` will download a compressed file that contains the necessary files to authenticate with IMCO API and manage your infrastructure. `Keep it safe`. Extract the contents with your zip compressor of choice and continue using the setup guide for your O.S. @@ -61,7 +61,7 @@ Extract the contents with your zip compressor of choice and continue using the s ### Configuration -IMCO CLI configuration will usually be located in your personal folder under `.concerto`. If you are using root, CLI will look for contiguration files under `/etc/imco`. +IMCO CLI configuration will usually be located in your personal folder under `.concerto`. If you are using root, CLI will look for contiguration files under `/etc/cio`. We will assume that you are not root, so create the folder and drop the certificates to this location: ```bash @@ -131,15 +131,16 @@ USAGE: concerto [global options] command [command options] [arguments...] VERSION: - 0.6.1 + 0.7.0 AUTHOR: Concerto Contributors COMMANDS: blueprint, bl Manages blueprint commands for scripts, services and templates - cloud, clo Manages cloud related commands for workspaces, servers, generic images, ssh profiles, cloud providers, server plans and Saas providers + cloud, clo Manages cloud related commands for servers, generic images, ssh profiles, cloud providers, server plans and Saas providers events, ev Events allow the user to track their actions and the state of their servers + labels, lbl Provides information about labels network, net Manages network related commands for firewall profiles settings, set Provides settings for cloud accounts setup, se Configures and setups concerto cli enviroment @@ -147,12 +148,17 @@ COMMANDS: ... ``` -To test that certificates are valid, and that we can communicate with IMCO server, obtain the list of workspaces at your IMCO account using this command +To test that certificates are valid, and that we can communicate with IMCO server, obtain the list of cloud providers at your IMCO account using this command ```bash -$ concerto cloud workspaces list -ID NAME DEFAULT SSH_PROFILE_ID FIREWALL_PROFILE_ID -5aabb7521de0240abb00000e default true 5aabb7521de0240abb00000d 5aabb7521de0240abb00000c +$ concerto cloud cloud_providers list +ID NAME +5aabb7511de0240abb000001 AWS +5aabb7511de0240abb000002 Mock +5aabb7511de0240abb000003 DigitalOcean +5aabb7511de0240abb000004 Microsoft Azure ARM +5aabb7511de0240abb000005 Microsoft Azure +5aba04be425b5d0c16000000 VCloud ``` ## Environment variables @@ -178,12 +184,14 @@ If you got an error executing IMCO CLI: - check that your internet connection can reach `clients.{IMCO_DOMAIN}` - make sure that your firewall lets you access to - check that `client.xml` is pointing to the correct certificates location -- if `concerto` executes but only shows server commands, you are probably trying to use `concerto` from a commissioned server, and the configuration is being read from `/etc/imco`. If that's the case, you should leave `concerto` configuration untouched so that server commands are available for our remote management. +- if `concerto` executes but only shows server commands, you are probably trying to use `concerto` from a commissioned server, and the configuration is being read from `/etc/cio`. If that's the case, you should leave `concerto` configuration untouched so that server commands are available for our remote management. ## Usage We include the most common use cases here. If you feel there is a missing a use case here, open an github issue . +From release 0.7.0 the resources can be organized using labels, a many-to-many relationship between labels and resources, based on User criteria and needs ('workspaces' are not available anymore) + ## Wizard The Wizard command for IMCO CLI is the command line version of our `Quick add server` in the IMCO's Web UI. @@ -281,46 +289,32 @@ FLAVOUR_REQUIREMENTS: GENERIC_IMAGE_ID: ``` -We have a new server template and a workspace with a commissioned server in IMCO. +We have a new server template with a commissioned server in IMCO. Server Commissioned -From the command line, get the new workspace, and then our commissioned server ID. - -```bash -$ concerto cloud workspaces list -ID NAME DEFAULT SSH_PROFILE_ID FIREWALL_PROFILE_ID -5aabb7521de0240abb00000e default true 5aabb7521de0240abb00000d 5aabb7521de0240abb00000c -5b0ea6377906e900fab96797 Wordpress_workspace false 5aabb7521de0240abb00000d 5b0ea6377906e900fab96795 -``` - -```bash -$ concerto cloud workspaces list_workspace_servers --workspace_id 5b0ea6377906e900fab96797 -ID NAME FQDN STATE PUBLIC_IP WORKSPACE_ID TEMPLATE_ID SERVER_PLAN_ID SSH_PROFILE_ID -5b0ea6377906e900fab96798 wpnode1 sf98aa2c61069a1b.centralus.cloudapp.azure.com inactive 104.43.245.138 5b0ea6377906e900fab96797 5b0ea6377906e900fab96792 5aac0c05348f190b3e0011c2 5aabb7521de0240abb00000d -``` - Our server's ID is `5b0ea6377906e900fab96798`. We can now use `concerto cloud servers` subcommands to manage the server. Lets bring wordpress up: ```bash $ concerto cloud servers boot --id 5b0ea6377906e900fab96798 -ID: 5b0ea6377906e900fab96798 -NAME: wpnode1 -FQDN: sf98aa2c61069a1b.centralus.cloudapp.azure.com -STATE: booting -PUBLIC_IP: 104.43.245.138 -WORKSPACE_ID: 5b0ea6377906e900fab96797 -TEMPLATE_ID: 5b0ea6377906e900fab96792 -SERVER_PLAN_ID: 5aac0c05348f190b3e0011c2 -CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 -SSH_PROFILE_ID: 5aabb7521de0240abb00000d +ID: 5b0ea6377906e900fab96798 +NAME: wpnode1 +FQDN: sf98aa2c61069a1b.centralus.cloudapp.azure.com +STATE: booting +PUBLIC_IP: 104.43.245.138 +TEMPLATE_ID: 5b0ea6377906e900fab96792 +SERVER_PLAN_ID: 5aac0c05348f190b3e0011c2 +CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 +SSH_PROFILE_ID: 5aabb7521de0240abb00000e +FIREWALL_PROFILE_ID: 5b50a4c75f7c880ad9c6bbfb +RESOURCE_TYPE: server +LABELS: [Wordpress] ``` Server status: `Bootstraping` Server Bootstraping - Server status: `Operational` Server Operational @@ -359,6 +353,7 @@ ID NAME 5aabb7551de0240abb000068 Red Hat Enterprise Linux 7.3 x86_64 5aabb7551de0240abb000069 CentOS 7.4 x86_64 5aabb7551de0240abb00006a Debian 9 x86_64 +5b2a331ee09b740b5ee72f24 Ubuntu 18.04 Bionic Beaver x86_64 ``` Take note of Ubuntu 16.04 ID, `5aabb7551de0240abb000065`. @@ -376,29 +371,22 @@ ID NAME DESCRIPTION Joomla curated cookbooks creates a local mysql database. We only have to tell our cookbook that we should override the `joomla.db.hostname` to `127.0.0.1`. Execute the following command to create the Joomla template. ```bash -$ concerto blueprint templates create --name joomla-tmplt --generic_image_id 5aabb7551de0240abb000065 --service_list '["joomla"]' --configuration_attributes '{"joomla":{"db":{"hostname":"127.0.0.1"}}}' -ID: 5b0ebc6e7906e900fab967a3 +$ concerto blueprint templates create --name joomla-tmplt --generic_image_id 5aabb7551de0240abb000065 --service_list '["joomla"]' --configuration_attributes '{"joomla":{"db":{"hostname":"127.0.0.1"}}}' --labels Joomla,mysite.com +ID: 5b5192b15f7c880ad9c6bc12 NAME: joomla-tmplt GENERIC IMAGE ID: 5aabb7551de0240abb000065 SERVICE LIST: [joomla] CONFIGURATION ATTRIBUTES: {"joomla":{"db":{"hostname":"127.0.0.1"}}} +RESOURCE_TYPE: template +LABELS: [mysite.com Joomla] ``` #### Instantiate a server -Now that we have our server blueprint defined, let's start one. Servers in IMCO need to know the workspace that define their runtime infrastructure environment, the server plan for the cloud provider, and the template used to build the instance. +Now that we have our server blueprint defined, let's start one. Servers in IMCO need to know the server plan for the cloud provider, and the template used to build the instance. As we did in the Wizard use case, we can find the missing data using these commands: -##### Find the workspace - -```bash -$ concerto cloud workspaces list -ID NAME DEFAULT SSH_PROFILE_ID FIREWALL_PROFILE_ID -5aabb7521de0240abb00000e default true 5aabb7521de0240abb00000d 5aabb7521de0240abb00000c -5b0ea6377906e900fab96797 Wordpress_workspace false 5aabb7521de0240abb00000d 5b0ea6377906e900fab96795 -``` - ##### Find cloud provider server plan ```bash @@ -451,11 +439,11 @@ We already know our template ID, but in case you want to make sure ```bash $ concerto blueprint templates list -ID NAME GENERIC IMAGE ID -5afd5b4c42d90d09f00000aa windows 2016 5aabb7551de0240abb000067 -5b067fe8f585000b80809a8e ubuntu 16.04 5aabb7551de0240abb000065 -5b0ea6377906e900fab96792 Wordpress_template 5aabb7551de0240abb000064 -5b0ebc6e7906e900fab967a3 joomla-tmplt 5aabb7551de0240abb000065 +ID NAME GENERIC IMAGE ID LABELS +5afd5b4c42d90d09f00000aa windows 2016 5aabb7551de0240abb000067 [] +5b067fe8f585000b80809a8e ubuntu 16.04 5aabb7551de0240abb000065 [] +5b0ea6377906e900fab96792 Wordpress_template 5aabb7551de0240abb000064 [Wordpress] +5b5192b15f7c880ad9c6bc12 joomla-tmplt 5aabb7551de0240abb000065 [mysite.com Joomla] ``` ##### Find Location ID @@ -473,7 +461,7 @@ ID NAME ##### Find Cloud Account ID -It's necessary to retrive the adequeate Cloud Account ID for `Microsoft Azure` Cloud Provider, in our case `5aabb7511de0240abb000005`: +It's necessary to retrieve the adequate Cloud Account ID for `Microsoft Azure` Cloud Provider, in our case `5aabb7511de0240abb000005`: ```bash $ concerto settings cloud_accounts list @@ -489,57 +477,87 @@ ID NAME CLOUD_PROVID 5aba066c425b5d0c64000002 VMWare-Routed-cloud_account-name 5aba04be425b5d0c16000000 VCloud ``` +##### Find SSH Profile ID + +It's necessary to retrieve the adequate SSH Profile ID. It can be created using CLI commands or IMCO UI. + +```bash +$ concerto cloud ssh_profiles list +ID NAME PUBLIC_KEY LABELS +5aabb7521de0240abb00000d default ssh-rsa AAAAB3NzaC1yc[...] [] +5aabb7521de0240abb00000e Joomla SSH ssh-rsa AAAABBfD4Klmn[...] [mysite.com Joomla] +[...] +``` + +##### Find Firewall Profile ID + +It's necessary to retrieve the adequate Firewall Profile ID. It can be created using CLI commands or IMCO UI. + +```bash +$ concerto network firewall_profiles list +ID NAME DESCRIPTION DEFAULT LABELS +5aabb7521de0240abb00000c Default firewall Firewall profile created by the platfom for your use true [] +5b519da77fb2480b0831d9d2 Joomla Firewall Firewall profile created for joomla management false [mysite.com Joomla] +[...] +``` + ##### Create our Joomla Server ```bash -$ concerto cloud servers create --name joomla-node1 --workspace_id 5aabb7521de0240abb00000e --template_id 5b0ebc6e7906e900fab967a3 --server_plan_id 5aac0c04348f190b3e001186 --cloud_account_id 5aabb7531de0240abb000024 -ID: 5b0ebe297906e900fab967a7 -NAME: joomla-node1 +$ concerto cloud servers create --name joomla-node1 --template_id 5b5192b15f7c880ad9c6bc12 --server_plan_id 5aac0c04348f190b3e001186 --cloud_account_id 5aabb7531de0240abb000024 --ssh_profile_id 5aabb7521de0240abb00000e --firewall_profile_id 5b519da77fb2480b0831d9d2 --labels Joomla,mysite.com +ID: 5b5193675f7c880ad9c6bc16 +NAME: joomla-node1 FQDN: -STATE: commissioning +STATE: commissioning PUBLIC_IP: -WORKSPACE_ID: 5aabb7521de0240abb00000e -TEMPLATE_ID: 5b0ebc6e7906e900fab967a3 -SERVER_PLAN_ID: 5aac0c04348f190b3e001186 -CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 -SSH_PROFILE_ID: 5aabb7521de0240abb00000d +TEMPLATE_ID: 5b5192b15f7c880ad9c6bc12 +SERVER_PLAN_ID: 5aac0c04348f190b3e001186 +CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 +SSH_PROFILE_ID: 5aabb7521de0240abb00000e +FIREWALL_PROFILE_ID: 5b519da77fb2480b0831d9d2 +RESOURCE_TYPE: server +LABELS: [mysite.com Joomla] ``` And finally boot it ```bash -$ concerto cloud servers boot --id 5b0ebe297906e900fab967a7 -ID: 5b0ebe297906e900fab967a7 -NAME: joomla-node1 +$ concerto cloud servers boot --id 5b5193675f7c880ad9c6bc16 +ID: 5b5193675f7c880ad9c6bc16 +NAME: joomla-node1 FQDN: -STATE: booting +STATE: booting PUBLIC_IP: -WORKSPACE_ID: 5aabb7521de0240abb00000e -TEMPLATE_ID: 5b0ebc6e7906e900fab967a3 -SERVER_PLAN_ID: 5aac0c04348f190b3e001186 -CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 -SSH_PROFILE_ID: 5aabb7521de0240abb00000d +TEMPLATE_ID: 5b5192b15f7c880ad9c6bc12 +SERVER_PLAN_ID: 5aac0c04348f190b3e001186 +CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 +SSH_PROFILE_ID: 5aabb7521de0240abb00000e +FIREWALL_PROFILE_ID: 5b519da77fb2480b0831d9d2 +RESOURCE_TYPE: server +LABELS: [mysite.com Joomla] ``` You can retrieve the current status of the server and see how it transitions along different statuses (booting, bootstrapping, operational). Then, after a brief amount of time the final status is reached: ```bash -$ concerto cloud servers show --id 5b0ebe297906e900fab967a7 -ID: 5b0ebe297906e900fab967a7 -NAME: joomla-node1 -FQDN: s22e6c216adaec08.centralus.cloudapp.azure.com -STATE: operational -PUBLIC_IP: 104.43.242.14 -WORKSPACE_ID: 5aabb7521de0240abb00000e -TEMPLATE_ID: 5b0ebc6e7906e900fab967a3 -SERVER_PLAN_ID: 5aac0c04348f190b3e001186 -CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 -SSH_PROFILE_ID: 5aabb7521de0240abb00000d +$ concerto cloud servers show --id 5b5193675f7c880ad9c6bc16 +ID: 5b5193675f7c880ad9c6bc16 +NAME: joomla-node1 +FQDN: s6ef3f68038ec9e8.centralus.cloudapp.azure.com +STATE: operational +PUBLIC_IP: 23.99.252.146 +TEMPLATE_ID: 5b5192b15f7c880ad9c6bc12 +SERVER_PLAN_ID: 5aac0c04348f190b3e001186 +CLOUD_ACCOUNT_ID: 5aabb7531de0240abb000024 +SSH_PROFILE_ID: 5aabb7521de0240abb00000e +FIREWALL_PROFILE_ID: 5b519da77fb2480b0831d9d2 +RESOURCE_TYPE: server +LABELS: [mysite.com Joomla] ``` ## Firewall Management -IMCO CLI's `network` command lets you manage a network settings at the workspace scope. +IMCO CLI's `network` command lets you manage a network settings at the server scope. As we have did before, execute this command with no futher commands to get usage information: @@ -559,36 +577,28 @@ As you can see, you can manage firewall from IMCO CLI. ### Firewall Update Case -Workspaces in IMCO are always associated with a firewall profile. By default ports 443 and 80 are open to fit most web environments, but if you are not using those ports but some others. We would need to close HTTP and HTTPS ports and open LDAP and LDAPS instead. - -The first thing we will need is our workspace's related firewall identifier. - -```bash -$ concerto cloud workspaces list -ID NAME DEFAULT SSH_PROFILE_ID FIREWALL_PROFILE_ID -5aabb7521de0240abb00000e default true 5aabb7521de0240abb00000d 5aabb7521de0240abb00000c -5b0ea6377906e900fab96797 Wordpress_workspace false 5aabb7521de0240abb00000d 5b0ea6377906e900fab96795 -5b0ec2e594771f0b76361dd9 My New Workspace false 5aabb7521de0240abb00000d 5b0ec2c994771f0b76361dd6 -``` +Servers in IMCO are always associated with a firewall profile. By default ports 443 and 80 are open to fit most web environments, but if you are not using those ports but some others. We would need to close HTTP and HTTPS ports and open LDAP and LDAPS instead. -We have our LDAP servers running under `My New Workspace`. If you are unsure about in which workspace are your servers running, list the servers in the workspace +The first thing we will need is our servers's related firewall identifier. In this they can be found filtering by label assigned 'LDAP': ```bash -concerto cloud workspaces list_workspace_servers --workspace_id 5b0ec2e594771f0b76361dd9 -ID NAME FQDN STATE PUBLIC_IP WORKSPACE_ID TEMPLATE_ID SERVER_PLAN_ID SSH_PROFILE_ID -5b0ec38494771f0b76361ddb openldap-1 inactive 5b0ec2e594771f0b76361dd9 5b067fe8f585000b80809a8e 5aabb76fe499780a0000059a 5aabb7521de0240abb00000d -5b0ec3a294771f0b76361dde openldap-2 scd9ee0721949943.northcentralus.cloudapp.azure.com operational 23.101.166.77 5b0ec2e594771f0b76361dd9 5b067fe8f585000b80809a8e 5aac0c0e348f190b3e001433 5aabb7521de0240abb00000d +$ concerto cloud servers list --labels LDAP +ID NAME FQDN STATE PUBLIC_IP TEMPLATE_ID SERVER_PLAN_ID CLOUD_ACCOUNT_ID SSH_PROFILE_ID FIREWALL_PROFILE_ID LABELS +5b51a9dc7fb2480b0831d9eb openldap-1 inactive 5afd5b4c42d90d09f00000aa 5aac0c0e348f190b3e001432 5aabb7531de0240abb000024 5b51a9617fb2480b0831d9e9 5b51a9377fb2480b0831d9e6 [LDAP] +5b51a9ff7fb2480b0831d9ee openldap-2 sca9229d77b151d4.northcentralus.cloudapp.azure.com operational 23.100.76.238 5afd5b4c42d90d09f00000aa 5aac0c0e348f190b3e001432 5aabb7531de0240abb000024 5b51a9617fb2480b0831d9e9 5b51a9377fb2480b0831d9e6 [LDAP] ``` Now that we have the firewall profile ID, list it's contents ```bash -$ concerto network firewall_profiles show --id 5b0ec2c994771f0b76361dd6 -ID: 5b0ec2c994771f0b76361dd6 -NAME: My New Firewall Profile -DESCRIPTION: -DEFAULT: false -RULES: [{Protocol:tcp MinPort:22 MaxPort:22 CidrIp:any} {Protocol:tcp MinPort:5985 MaxPort:5985 CidrIp:any} {Protocol:tcp MinPort:3389 MaxPort:3389 CidrIp:any} {Protocol:tcp MinPort:10050 MaxPort:10050 CidrIp:any} {Protocol:tcp MinPort:443 MaxPort:443 CidrIp:any} {Protocol:tcp MinPort:80 MaxPort:80 CidrIp:any}] +$ concerto network firewall_profiles show --id 5b51a9377fb2480b0831d9e6 +ID: 5b51a9377fb2480b0831d9e6 +NAME: Firewall LDAP +DESCRIPTION: LDAP Services firewall +DEFAULT: false +RULES: [{Protocol:tcp MinPort:22 MaxPort:22 CidrIP:any} {Protocol:tcp MinPort:5985 MaxPort:5985 CidrIP:any} {Protocol:tcp MinPort:3389 MaxPort:3389 CidrIP:any} {Protocol:tcp MinPort:10050 MaxPort:10050 CidrIP:any} {Protocol:tcp MinPort:443 MaxPort:443 CidrIP:any} {Protocol:tcp MinPort:80 MaxPort:80 CidrIP:any}] +RESOURCE_TYPE: firewall_profile +LABELS: [LDAP] ``` The first four values are ports that IMCO may use to keep the desired state of the machine, and that will always be accessed using certificates. @@ -596,12 +606,14 @@ The first four values are ports that IMCO may use to keep the desired state of t When updating, we tell IMCO a new set of rules. Execute the following command to open 389 and 686 to anyone. ```bash -$ concerto network firewall_profiles update --id 5b0ec2c994771f0b76361dd6 --rules '[{"ip_protocol":"tcp", "min_port":389, "max_port":389, "source":"0.0.0.0/0"}, {"ip_protocol":"tcp", "min_port":636, "max_port":636, "source":"0.0.0.0/0"}]' -ID: 5b0ec2c994771f0b76361dd6 -NAME: My New Firewall Profile -DESCRIPTION: -DEFAULT: false -RULES: [{Protocol:tcp MinPort:22 MaxPort:22 CidrIp:any} {Protocol:tcp MinPort:5985 MaxPort:5985 CidrIp:any} {Protocol:tcp MinPort:3389 MaxPort:3389 CidrIp:any} {Protocol:tcp MinPort:10050 MaxPort:10050 CidrIp:any} {Protocol:tcp MinPort:389 MaxPort:389 CidrIp:any} {Protocol:tcp MinPort:636 MaxPort:636 CidrIp:any}] +$ concerto network firewall_profiles update --id 5b51a9377fb2480b0831d9e6 --rules '[{"ip_protocol":"tcp", "min_port":389, "max_port":389, "source":"0.0.0.0/0"}, {"ip_protocol":"tcp", "min_port":636, "max_port":636, "source":"0.0.0.0/0"}]' +ID: 5b51a9377fb2480b0831d9e6 +NAME: Firewall LDAP +DESCRIPTION: LDAP Services firewall +DEFAULT: false +RULES: [{Protocol:tcp MinPort:22 MaxPort:22 CidrIP:any} {Protocol:tcp MinPort:5985 MaxPort:5985 CidrIP:any} {Protocol:tcp MinPort:3389 MaxPort:3389 CidrIP:any} {Protocol:tcp MinPort:10050 MaxPort:10050 CidrIP:any} {Protocol:tcp MinPort:389 MaxPort:389 CidrIP:any} {Protocol:tcp MinPort:636 MaxPort:636 CidrIP:any}] +RESOURCE_TYPE: firewall_profile +LABELS: [LDAP] ``` Firewall update returns the complete set of rules. As you can see, now LDAP and LDAPS ports are open. @@ -617,45 +629,53 @@ Let's pretend there is an existing Joomla blueprint, and that we want to update This is the Joomla blueprint that we created in a previous use case. ```bash -$ concerto blueprint templates show --id 5b0ebc6e7906e900fab967a3 -ID: 5b0ebc6e7906e900fab967a3 +$ concerto blueprint templates show --id 5b5192b15f7c880ad9c6bc12 +ID: 5b5192b15f7c880ad9c6bc12 NAME: joomla-tmplt GENERIC IMAGE ID: 5aabb7551de0240abb000065 SERVICE LIST: [joomla] CONFIGURATION ATTRIBUTES: {"joomla":{"db":{"hostname":"127.0.0.1"}}} +RESOURCE_TYPE: template +LABELS: [mysite.com Joomla] ``` Beware of adding previous services or configuration attributes. Update will replace existing items with the ones provided. If we don't want to lose the `joomla.db.hostname` attribute, add it to our configuretion attributes parameter: ```bash -$ concerto blueprint templates update --id 5b0ebc6e7906e900fab967a3 --configuration_attributes '{"joomla":{"db":{"hostname":"127.0.0.1", "password":"$afeP4sSw0rd"}}}' -ID: 5b0ebc6e7906e900fab967a3 +$ concerto blueprint templates update --id 5b5192b15f7c880ad9c6bc12 --configuration_attributes '{"joomla":{"db":{"hostname":"127.0.0.1", "password":"$afeP4sSw0rd"}}}' +ID: 5b5192b15f7c880ad9c6bc12 NAME: joomla-tmplt GENERIC IMAGE ID: 5aabb7551de0240abb000065 SERVICE LIST: [joomla] CONFIGURATION ATTRIBUTES: {"joomla":{"db":{"hostname":"127.0.0.1","password":"$afeP4sSw0rd"}}} +RESOURCE_TYPE: template +LABELS: [mysite.com Joomla] ``` As you can see, non specified parameters, like name and service list, remain unchanged. Let's now change the service list, adding a two cookbooks. ```bash -$ concerto blueprint templates update --id 5b0ebc6e7906e900fab967a3 --service_list '["joomla","python@1.4.6","polipo"]' -ID: 5b0ebc6e7906e900fab967a3 +$ concerto blueprint templates update --id 5b5192b15f7c880ad9c6bc12 --service_list '["joomla","python@1.4.6","polipo"]' +ID: 5b5192b15f7c880ad9c6bc12 NAME: joomla-tmplt GENERIC IMAGE ID: 5aabb7551de0240abb000065 SERVICE LIST: [joomla python@1.4.6 polipo] CONFIGURATION ATTRIBUTES: {"joomla":{"db":{"hostname":"127.0.0.1","password":"$afeP4sSw0rd"}}} +RESOURCE_TYPE: template +LABELS: [mysite.com Joomla] ``` Of course, we can change service list and configuration attributes in one command. ```bash -$ concerto blueprint templates update --id 5b0ebc6e7906e900fab967a3 --configuration_attributes '{"joomla":{"db":{"hostname":"127.0.0.1", "password":"$afeP4sSw0rd"}}}' --service_list '["joomla","python@1.4.6","polipo"]' -ID: 5b0ebc6e7906e900fab967a3 +$ concerto blueprint templates update --id 5b5192b15f7c880ad9c6bc12 --configuration_attributes '{"joomla":{"db":{"hostname":"127.0.0.1", "password":"$afeP4sSw0rd"}}}' --service_list '["joomla","python@1.4.6","polipo"]' +ID: 5b5192b15f7c880ad9c6bc12 NAME: joomla-tmplt GENERIC IMAGE ID: 5aabb7551de0240abb000065 SERVICE LIST: [joomla python@1.4.6 polipo] CONFIGURATION ATTRIBUTES: {"joomla":{"db":{"hostname":"127.0.0.1","password":"$afeP4sSw0rd"}}} +RESOURCE_TYPE: template +LABELS: [mysite.com Joomla] ``` ## Contribute diff --git a/api/blueprint/bootstrapping_api.go b/api/blueprint/bootstrapping_api.go new file mode 100644 index 0000000..14ab9f2 --- /dev/null +++ b/api/blueprint/bootstrapping_api.go @@ -0,0 +1,93 @@ +package blueprint + +import ( + "encoding/json" + "fmt" + + log "github.com/Sirupsen/logrus" + "github.com/ingrammicro/concerto/api/types" + "github.com/ingrammicro/concerto/utils" +) + + +// BootstrappingService manages bootstrapping operations +type BootstrappingService struct { + concertoService utils.ConcertoService +} + +// NewBootstrappingService returns a bootstrapping service +func NewBootstrappingService(concertoService utils.ConcertoService) (*BootstrappingService, error) { + if concertoService == nil { + return nil, fmt.Errorf("must initialize ConcertoService before using it") + } + + return &BootstrappingService{ + concertoService: concertoService, + }, nil + +} + +// GetBootstrappingConfiguration returns the list of policy files as a JSON response with the desired configuration changes +func (bs *BootstrappingService) GetBootstrappingConfiguration() (bootstrappingConfigurations *types.BootstrappingConfiguration, status int, err error) { + log.Debug("GetBootstrappingConfiguration") + + data, status, err := bs.concertoService.Get("/blueprint/configuration") + if err != nil { + return nil, status, err + } + + if err = utils.CheckStandardStatus(status, data); err != nil { + return nil, status, err + } + + if err = json.Unmarshal(data, &bootstrappingConfigurations); err != nil { + return nil, status, err + } + + return bootstrappingConfigurations, status, nil +} + +// ReportBootstrappingAppliedConfiguration +func (bs *BootstrappingService) ReportBootstrappingAppliedConfiguration(BootstrappingAppliedConfigurationVector *map[string]interface{}) (err error) { + log.Debug("ReportBootstrappingAppliedConfiguration") + + data, status, err := bs.concertoService.Put("/blueprint/applied_configuration", BootstrappingAppliedConfigurationVector) + if err != nil { + return err + } + + if err = utils.CheckStandardStatus(status, data); err != nil { + return err + } + + return nil +} + +// ReportBootstrappingLog reports a policy files application result +func (bs *BootstrappingService) ReportBootstrappingLog(BootstrappingContinuousReportVector *map[string]interface{}) (command *types.BootstrappingContinuousReport, status int, err error) { + log.Debug("ReportBootstrappingLog") + + data, status, err := bs.concertoService.Post("/blueprint/bootstrap_logs", BootstrappingContinuousReportVector) + if err != nil { + return nil, status, err + } + + if err = json.Unmarshal(data, &command); err != nil { + return nil, status, err + } + + return command, status, nil +} + + +// DownloadPolicyfile gets a file from given url saving file into given file path +func (bs *BootstrappingService) DownloadPolicyfile(url string, filePath string) (realFileName string, status int, err error) { + log.Debug("DownloadPolicyfile") + + realFileName, status, err = bs.concertoService.GetFile(url, filePath) + if err != nil { + return realFileName, status, err + } + + return realFileName, status, nil +} \ No newline at end of file diff --git a/api/blueprint/bootstrapping_api_mocked.go b/api/blueprint/bootstrapping_api_mocked.go new file mode 100644 index 0000000..916ddca --- /dev/null +++ b/api/blueprint/bootstrapping_api_mocked.go @@ -0,0 +1,358 @@ +package blueprint + +import ( + "encoding/json" + "fmt" + "github.com/ingrammicro/concerto/api/types" + "github.com/ingrammicro/concerto/utils" + "github.com/stretchr/testify/assert" + "testing" +) + +// GetBootstrappingConfigurationMocked test mocked function +func GetBootstrappingConfigurationMocked(t *testing.T, bcConfIn *types.BootstrappingConfiguration) *types.BootstrappingConfiguration { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dIn, err := json.Marshal(bcConfIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + // call service + cs.On("Get", "/blueprint/configuration").Return(dIn, 200, nil) + bcConfOut, status, err := ds.GetBootstrappingConfiguration() + assert.Nil(err, "Error getting bootstrapping configuration") + assert.Equal(status, 200, "GetBootstrappingConfiguration returned invalid response") + assert.Equal(bcConfIn, bcConfOut, "GetBootstrappingConfiguration returned different services") + return bcConfOut +} + +// GetBootstrappingConfigurationFailErrMocked test mocked function +func GetBootstrappingConfigurationFailErrMocked(t *testing.T, bcConfIn *types.BootstrappingConfiguration) *types.BootstrappingConfiguration { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dIn, err := json.Marshal(bcConfIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + // call service + cs.On("Get", "/blueprint/configuration").Return(dIn, 404, fmt.Errorf("Mocked error")) + bcConfOut, _, err := ds.GetBootstrappingConfiguration() + + assert.NotNil(err, "We are expecting an error") + assert.Nil(bcConfOut, "Expecting nil output") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") + + return bcConfOut +} + +// GetBootstrappingConfigurationFailStatusMocked test mocked function +func GetBootstrappingConfigurationFailStatusMocked(t *testing.T, bcConfIn *types.BootstrappingConfiguration) *types.BootstrappingConfiguration { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dIn, err := json.Marshal(bcConfIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + // call service + cs.On("Get", "/blueprint/configuration").Return(dIn, 499, nil) + bcConfOut, status, err := ds.GetBootstrappingConfiguration() + + assert.NotNil(err, "We are expecting an status code error") + assert.Nil(bcConfOut, "Expecting nil output") + assert.Equal(499, status, "Expecting http code 499") + assert.Contains(err.Error(), "499", "Error should contain http code 499") + + return bcConfOut +} + +// GetBootstrappingConfigurationFailJSONMocked test mocked function +func GetBootstrappingConfigurationFailJSONMocked(t *testing.T, bcConfIn *types.BootstrappingConfiguration) *types.BootstrappingConfiguration { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // wrong json + dIn := []byte{10, 20, 30} + + // call service + cs.On("Get", "/blueprint/configuration").Return(dIn, 200, nil) + bcConfOut, _, err := ds.GetBootstrappingConfiguration() + + assert.NotNil(err, "We are expecting a marshalling error") + assert.Nil(bcConfOut, "Expecting nil output") + + return bcConfOut +} + +// ReportBootstrappingAppliedConfigurationMocked test mocked function +func ReportBootstrappingAppliedConfigurationMocked(t *testing.T, commandIn *types.BootstrappingConfiguration) { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dOut, err := json.Marshal(commandIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + // call service + payload := make(map[string]interface{}) + cs.On("Put", fmt.Sprintf("/blueprint/applied_configuration"), &payload).Return(dOut, 200, nil) + err = ds.ReportBootstrappingAppliedConfiguration(&payload) + assert.Nil(err, "Error getting bootstrapping command") +} + +// ReportBootstrappingAppliedConfigurationFailErrMocked test mocked function +func ReportBootstrappingAppliedConfigurationFailErrMocked(t *testing.T, commandIn *types.BootstrappingConfiguration) { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dIn, err := json.Marshal(commandIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + dIn = nil + + // call service + payload := make(map[string]interface{}) + cs.On("Put", fmt.Sprintf("/blueprint/applied_configuration"), &payload).Return(dIn, 499, fmt.Errorf("Mocked error")) + err = ds.ReportBootstrappingAppliedConfiguration(&payload) + assert.NotNil(err, "We are expecting an error") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") +} + +// ReportBootstrappingAppliedConfigurationFailStatusMocked test mocked function +func ReportBootstrappingAppliedConfigurationFailStatusMocked(t *testing.T, commandIn *types.BootstrappingConfiguration) { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dIn, err := json.Marshal(commandIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + dIn = nil + + // call service + payload := make(map[string]interface{}) + cs.On("Put", fmt.Sprintf("/blueprint/applied_configuration"), &payload).Return(dIn, 499, fmt.Errorf("Error 499 Mocked error")) + err = ds.ReportBootstrappingAppliedConfiguration(&payload) + assert.NotNil(err, "We are expecting a status code error") + assert.Contains(err.Error(), "499", "Error should contain http code 499") +} + +// ReportBootstrappingAppliedConfigurationFailJSONMocked test mocked function +func ReportBootstrappingAppliedConfigurationFailJSONMocked(t *testing.T, commandIn *types.BootstrappingConfiguration) { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // wrong json + dIn := []byte{0} + + // call service + payload := make(map[string]interface{}) + cs.On("Put", fmt.Sprintf("/blueprint/applied_configuration"), &payload).Return(dIn, 499, nil) + err = ds.ReportBootstrappingAppliedConfiguration(&payload) + assert.Contains(err.Error(), "499", "Error should contain http code 499") +} + +// ReportBootstrappingLogMocked test mocked function +func ReportBootstrappingLogMocked(t *testing.T, commandIn *types.BootstrappingContinuousReport) *types.BootstrappingContinuousReport { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dOut, err := json.Marshal(commandIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + // call service + payload := make(map[string]interface{}) + cs.On("Post", fmt.Sprintf("/blueprint/bootstrap_logs"), &payload).Return(dOut, 200, nil) + commandOut, status, err := ds.ReportBootstrappingLog(&payload) + + assert.Nil(err, "Error posting report command") + assert.Equal(status, 200, "ReportBootstrappingLog returned invalid response") + assert.Equal(commandOut.Stdout, "Bootstrap log created", "ReportBootstrapLog returned unexpected message") + + return commandOut +} + +// ReportBootstrappingLogFailErrMocked test mocked function +func ReportBootstrappingLogFailErrMocked(t *testing.T, commandIn *types.BootstrappingContinuousReport) *types.BootstrappingContinuousReport { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dIn, err := json.Marshal(commandIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + dIn = nil + + // call service + payload := make(map[string]interface{}) + cs.On("Post", fmt.Sprintf("/blueprint/bootstrap_logs"), &payload).Return(dIn, 499, fmt.Errorf("Mocked error")) + commandOut, _, err := ds.ReportBootstrappingLog(&payload) + + assert.NotNil(err, "We are expecting an error") + assert.Nil(commandOut, "Expecting nil output") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") + + return commandOut +} + +// ReportBootstrappingLogFailStatusMocked test mocked function +func ReportBootstrappingLogFailStatusMocked(t *testing.T, commandIn *types.BootstrappingContinuousReport) *types.BootstrappingContinuousReport { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // to json + dIn, err := json.Marshal(commandIn) + assert.Nil(err, "Bootstrapping test data corrupted") + + dIn = nil + + // call service + payload := make(map[string]interface{}) + cs.On("Post", fmt.Sprintf("/blueprint/bootstrap_logs"), &payload).Return(dIn, 499, fmt.Errorf("Error 499 Mocked error")) + commandOut, status, err := ds.ReportBootstrappingLog(&payload) + + assert.Equal(status, 499, "ReportBootstrappingLog returned an unexpected status code") + assert.NotNil(err, "We are expecting a status code error") + assert.Nil(commandOut, "Expecting nil output") + assert.Contains(err.Error(), "499", "Error should contain http code 499") + + return commandOut +} + +// ReportBootstrappingLogFailJSONMocked test mocked function +func ReportBootstrappingLogFailJSONMocked(t *testing.T, commandIn *types.BootstrappingContinuousReport) *types.BootstrappingContinuousReport { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + // wrong json + dIn := []byte{10, 20, 30} + + // call service + payload := make(map[string]interface{}) + cs.On("Post", fmt.Sprintf("/blueprint/bootstrap_logs"), &payload).Return(dIn, 200, nil) + commandOut, _, err := ds.ReportBootstrappingLog(&payload) + + assert.NotNil(err, "We are expecting a marshalling error") + assert.Nil(commandOut, "Expecting nil output") + assert.Contains(err.Error(), "invalid character", "Error message should include the string 'invalid character'") + + return commandOut +} + +// DownloadPolicyfileMocked +func DownloadPolicyfileMocked(t *testing.T, dataIn map[string]string) { + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + urlSource := dataIn["fakeURLToFile"] + pathFile := dataIn["fakeFileDownloadFile"] + + // call service + cs.On("GetFile", urlSource, pathFile).Return(pathFile, 200, nil) + realFileName, status, err := ds.DownloadPolicyfile(urlSource, pathFile) + assert.Nil(err, "Error downloading bootstrapping policy file") + assert.Equal(status, 200, "DownloadPolicyfile returned invalid response") + assert.Equal(realFileName, pathFile, "Invalid downloaded file path") +} + +// DownloadPolicyfileFailErrMocked +func DownloadPolicyfileFailErrMocked(t *testing.T, dataIn map[string]string) { + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewBootstrappingService(cs) + assert.Nil(err, "Couldn't load bootstrapping service") + assert.NotNil(ds, "Bootstrapping service not instanced") + + urlSource := dataIn["fakeURLToFile"] + pathFile := dataIn["fakeFileDownloadFile"] + + // call service + cs.On("GetFile", urlSource, pathFile).Return("", 499, fmt.Errorf("Mocked error")) + _, status, err := ds.DownloadPolicyfile(urlSource, pathFile) + assert.NotNil(err, "We are expecting an error") + assert.Equal(status, 499, "DownloadPolicyfile returned an unexpected status code") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") +} diff --git a/api/blueprint/bootstrapping_api_test.go b/api/blueprint/bootstrapping_api_test.go new file mode 100644 index 0000000..8815905 --- /dev/null +++ b/api/blueprint/bootstrapping_api_test.go @@ -0,0 +1,44 @@ +package blueprint + +import ( + "github.com/ingrammicro/concerto/testdata" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewBootstrappingServiceNil(t *testing.T) { + assert := assert.New(t) + rs, err := NewBootstrappingService(nil) + assert.Nil(rs, "Uninitialized service should return nil") + assert.NotNil(err, "Uninitialized service should return error") +} + +func TestGetBootstrappingConfiguration(t *testing.T) { + bcIn := testdata.GetBootstrappingConfigurationData() + GetBootstrappingConfigurationMocked(t, bcIn) + GetBootstrappingConfigurationFailErrMocked(t, bcIn) + GetBootstrappingConfigurationFailStatusMocked(t, bcIn) + GetBootstrappingConfigurationFailJSONMocked(t, bcIn) +} + +func TestReportBootstrappingAppliedConfiguration(t *testing.T) { + bcIn := testdata.GetBootstrappingConfigurationData() + ReportBootstrappingAppliedConfigurationMocked(t, bcIn) + ReportBootstrappingAppliedConfigurationFailErrMocked(t, bcIn) + ReportBootstrappingAppliedConfigurationFailStatusMocked(t, bcIn) + ReportBootstrappingAppliedConfigurationFailJSONMocked(t, bcIn) +} + +func TestReportBootstrappingLog(t *testing.T) { + commandIn := testdata.GetBootstrappingContinuousReportData() + ReportBootstrappingLogMocked(t, commandIn) + ReportBootstrappingLogFailErrMocked(t, commandIn) + ReportBootstrappingLogFailStatusMocked(t, commandIn) + ReportBootstrappingLogFailJSONMocked(t, commandIn) +} + +func TestDownloadPolicyfile(t *testing.T) { + dataIn := testdata.GetBootstrappingDownloadFileData() + DownloadPolicyfileMocked(t, dataIn) + DownloadPolicyfileFailErrMocked(t, dataIn) +} diff --git a/api/labels/labels_api.go b/api/labels/labels_api.go new file mode 100644 index 0000000..380436f --- /dev/null +++ b/api/labels/labels_api.go @@ -0,0 +1,110 @@ +package labels + +import ( + "encoding/json" + "fmt" + + log "github.com/Sirupsen/logrus" + "github.com/ingrammicro/concerto/api/types" + "github.com/ingrammicro/concerto/utils" +) + +// LabelService manages polling operations +type LabelService struct { + concertoService utils.ConcertoService +} + +// NewLabelService returns a Concerto labels service +func NewLabelService(concertoService utils.ConcertoService) (*LabelService, error) { + if concertoService == nil { + return nil, fmt.Errorf("Must initialize ConcertoService before using it") + } + + return &LabelService{ + concertoService: concertoService, + }, nil +} + +// GetLabelList returns the list of labels as an array of Label +func (lbl *LabelService) GetLabelList() (labels []types.Label, err error) { + log.Debug("GetLabelList") + + data, status, err := lbl.concertoService.Get("/v1/labels") + if err != nil { + return nil, err + } + + if err = utils.CheckStandardStatus(status, data); err != nil { + return nil, err + } + + if err = json.Unmarshal(data, &labels); err != nil { + return nil, err + } + + // exclude internal labels (with a Namespace defined) + var filteredLabels []types.Label + for _, label := range labels { + if label.Namespace == "" { + filteredLabels = append(filteredLabels, label) + } + } + + return filteredLabels, nil +} + +// CreateLabel creates a label +func (lbl *LabelService) CreateLabel(labelVector *map[string]interface{}) (label *types.Label, err error) { + log.Debug("CreateLabel") + + data, status, err := lbl.concertoService.Post("/v1/labels/", labelVector) + if err != nil { + return nil, err + } + + if err = utils.CheckStandardStatus(status, data); err != nil { + return nil, err + } + + if err = json.Unmarshal(data, &label); err != nil { + return nil, err + } + + return label, nil +} + +// AddLabel assigns a single label from a single labelable resource +func (lbl *LabelService) AddLabel(labelVector *map[string]interface{}, labelID string) (labeledResources []types.LabeledResource, err error) { + log.Debug("AddLabel") + + data, status, err := lbl.concertoService.Post(fmt.Sprintf("/v1/labels/%s/resources", labelID), labelVector) + if err != nil { + return nil, err + } + + if err = utils.CheckStandardStatus(status, data); err != nil { + return nil, err + } + + if err = json.Unmarshal(data, &labeledResources); err != nil { + return nil, err + } + + return labeledResources, nil +} + +// RemoveLabel de-assigns a single label from a single labelable resource +func (lbl *LabelService) RemoveLabel(labelID string, resourceType string, resourceID string) error { + log.Debug("RemoveLabel") + + data, status, err := lbl.concertoService.Delete(fmt.Sprintf("v1/labels/%s/resources/%s/%s", labelID, resourceType, resourceID)) + if err != nil { + return err + } + + if err = utils.CheckStandardStatus(status, data); err != nil { + return err + } + + return nil +} diff --git a/api/labels/labels_api_mocked.go b/api/labels/labels_api_mocked.go new file mode 100644 index 0000000..bbde988 --- /dev/null +++ b/api/labels/labels_api_mocked.go @@ -0,0 +1,437 @@ +package labels + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ingrammicro/concerto/api/types" + "github.com/ingrammicro/concerto/utils" + "github.com/stretchr/testify/assert" +) + +// GetLabelListMocked test mocked function +func GetLabelListMocked(t *testing.T, labelsIn *[]types.Label) *[]types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // to json + dIn, err := json.Marshal(labelsIn) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Get", "/v1/labels").Return(dIn, 200, nil) + labelsOut, err := ds.GetLabelList() + assert.Nil(err, "Error getting labels list") + assert.Equal(*labelsIn, labelsOut, "GetLabelList returned different labels") + + return &labelsOut +} + +// GetLabelListMockedWithNamespace test mocked function +func GetLabelListMockedWithNamespace(t *testing.T, labelsIn *[]types.Label) *[]types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // to json + dIn, err := json.Marshal(labelsIn) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Get", "/v1/labels").Return(dIn, 200, nil) + labelsOut, err := ds.GetLabelList() + assert.Nil(err, "Error getting labels list") + assert.NotEqual(*labelsIn, labelsOut, "GetLabelList returned labels with Namespaces") + + return &labelsOut +} + +// GetLabelListFailErrMocked test mocked function +func GetLabelListFailErrMocked(t *testing.T, labelsIn *[]types.Label) *[]types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // to json + dIn, err := json.Marshal(labelsIn) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Get", "/v1/labels").Return(dIn, 200, fmt.Errorf("Mocked error")) + labelsOut, err := ds.GetLabelList() + + assert.NotNil(err, "We are expecting an error") + assert.Nil(labelsOut, "Expecting nil output") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") + + return &labelsOut +} + +// GetLabelListFailStatusMocked test mocked function +func GetLabelListFailStatusMocked(t *testing.T, labelsIn *[]types.Label) *[]types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // to json + dIn, err := json.Marshal(labelsIn) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Get", "/v1/labels").Return(dIn, 499, nil) + labelsOut, err := ds.GetLabelList() + + assert.NotNil(err, "We are expecting an status code error") + assert.Nil(labelsOut, "Expecting nil output") + assert.Contains(err.Error(), "499", "Error should contain http code 499") + + return &labelsOut +} + +// GetLabelListFailJSONMocked test mocked function +func GetLabelListFailJSONMocked(t *testing.T, labelsIn *[]types.Label) *[]types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // wrong json + dIn := []byte{10, 20, 30} + + // call service + cs.On("Get", "/v1/labels").Return(dIn, 200, nil) + labelsOut, err := ds.GetLabelList() + + assert.NotNil(err, "We are expecting a marshalling error") + assert.Nil(labelsOut, "Expecting nil output") + assert.Contains(err.Error(), "invalid character", "Error message should include the string 'invalid character'") + + return &labelsOut +} + +// CreateLabelMocked test mocked function +func CreateLabelMocked(t *testing.T, labelIn *types.Label) *types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // to json + dOut, err := json.Marshal(labelIn) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Post", "/v1/labels/", mapIn).Return(dOut, 200, nil) + labelOut, err := ds.CreateLabel(mapIn) + assert.Nil(err, "Error creating label") + assert.Equal(labelIn, labelOut, "CreateLabel returned different labels") + + return labelOut +} + +// CreateLabelFailErrMocked test mocked function +func CreateLabelFailErrMocked(t *testing.T, labelIn *types.Label) *types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // to json + dOut, err := json.Marshal(labelIn) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Post", "/v1/labels/", mapIn).Return(dOut, 200, fmt.Errorf("Mocked error")) + labelOut, err := ds.CreateLabel(mapIn) + + assert.NotNil(err, "We are expecting an error") + assert.Nil(labelOut, "Expecting nil output") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") + + return labelOut +} + +// CreateLabelFailStatusMocked test mocked function +func CreateLabelFailStatusMocked(t *testing.T, labelIn *types.Label) *types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // to json + dOut, err := json.Marshal(labelIn) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Post", "/v1/labels/", mapIn).Return(dOut, 499, nil) + labelOut, err := ds.CreateLabel(mapIn) + + assert.NotNil(err, "We are expecting an status code error") + assert.Nil(labelOut, "Expecting nil output") + assert.Contains(err.Error(), "499", "Error should contain http code 499") + + return labelOut +} + +// CreateLabelFailJSONMocked test mocked function +func CreateLabelFailJSONMocked(t *testing.T, labelIn *types.Label) *types.Label { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // wrong json + dIn := []byte{10, 20, 30} + + // call service + cs.On("Post", "/v1/labels/", mapIn).Return(dIn, 200, nil) + labelOut, err := ds.CreateLabel(mapIn) + + assert.NotNil(err, "We are expecting a marshalling error") + assert.Nil(labelOut, "Expecting nil output") + assert.Contains(err.Error(), "invalid character", "Error message should include the string 'invalid character'") + + return labelOut +} + +// AddLabelMocked test mocked function +func AddLabelMocked(t *testing.T, labelIn *types.Label, labeledResourcesOut []types.LabeledResource) []types.LabeledResource { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // to json + dOut, err := json.Marshal(labeledResourcesOut) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Post", fmt.Sprintf("/v1/labels/%s/resources", labelIn.ID), mapIn).Return(dOut, 200, nil) + labeledOut, err := ds.AddLabel(mapIn, labelIn.ID) + assert.Nil(err, "Error creating label") + assert.Equal(labeledOut, labeledResourcesOut, "CreateLabel returned invalid labeled resources") + + return labeledResourcesOut +} + +// AddLabelFailErrMocked test mocked function +func AddLabelFailErrMocked(t *testing.T, labelIn *types.Label, labeledResourcesOut []types.LabeledResource) []types.LabeledResource { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // to json + dOut, err := json.Marshal(labeledResourcesOut) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Post", fmt.Sprintf("/v1/labels/%s/resources", labelIn.ID), mapIn).Return(dOut, 200, fmt.Errorf("Mocked error")) + labeledOut, err := ds.AddLabel(mapIn, labelIn.ID) + assert.NotNil(err, "We are expecting an error") + assert.Nil(labeledOut, "Expecting nil output") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") + + return labeledResourcesOut +} + +// AddLabelFailStatusMocked test mocked function +func AddLabelFailStatusMocked(t *testing.T, labelIn *types.Label, labeledResourcesOut []types.LabeledResource) []types.LabeledResource { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // to json + dOut, err := json.Marshal(labeledResourcesOut) + assert.Nil(err, "Label test data corrupted") + + // call service + cs.On("Post", fmt.Sprintf("/v1/labels/%s/resources", labelIn.ID), mapIn).Return(dOut, 404, nil) + labeledOut, err := ds.AddLabel(mapIn, labelIn.ID) + assert.NotNil(err, "We are expecting an status code error") + assert.Nil(labeledOut, "Expecting nil output") + assert.Contains(err.Error(), "404", "Error should contain http code 404") + + return labeledResourcesOut +} + +// AddLabelFailJSONMocked test mocked function +func AddLabelFailJSONMocked(t *testing.T, labelIn *types.Label, labeledResourcesOut []types.LabeledResource) []types.LabeledResource { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // convertMap + mapIn, err := utils.ItemConvertParams(*labelIn) + assert.Nil(err, "Label test data corrupted") + + // wrong json + dOut := []byte{10, 20, 30} + + // call service + cs.On("Post", fmt.Sprintf("/v1/labels/%s/resources", labelIn.ID), mapIn).Return(dOut, 200, nil) + labeledOut, err := ds.AddLabel(mapIn, labelIn.ID) + assert.NotNil(err, "We are expecting a marshalling error") + assert.Nil(labeledOut, "Expecting nil output") + assert.Contains(err.Error(), "invalid character", "Error message should include the string 'invalid character'") + + return labeledResourcesOut +} + +// RemoveLabelMocked test mocked function +func RemoveLabelMocked(t *testing.T, labelIn *types.Label) { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // to json + dIn, err := json.Marshal(labelIn) + assert.Nil(err, "Label test data corrupted") + + // call service + resourceID := "5b5074735f7c880ad9c6bbce" + cs.On("Delete", fmt.Sprintf("v1/labels/%s/resources/%s/%s", labelIn.ID, labelIn.ResourceType, resourceID)).Return(dIn, 204, nil) + err = ds.RemoveLabel(labelIn.ID, labelIn.ResourceType, resourceID) + assert.Nil(err, "Error removing label") +} + +// RemoveLabelFailErrMocked test mocked function +func RemoveLabelFailErrMocked(t *testing.T, labelIn *types.Label) { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // to json + dIn, err := json.Marshal(labelIn) + assert.Nil(err, "Label test data corrupted") + + // call service + resourceID := "5b5074735f7c880ad9c6bbce" + cs.On("Delete", fmt.Sprintf("v1/labels/%s/resources/%s/%s", labelIn.ID, labelIn.ResourceType, resourceID)).Return(dIn, 204, fmt.Errorf("Mocked error")) + err = ds.RemoveLabel(labelIn.ID, labelIn.ResourceType, resourceID) + + assert.NotNil(err, "We are expecting an error") + assert.Equal(err.Error(), "Mocked error", "Error should be 'Mocked error'") +} + +// RemoveLabelFailStatusMocked test mocked function +func RemoveLabelFailStatusMocked(t *testing.T, labelIn *types.Label) { + + assert := assert.New(t) + + // wire up + cs := &utils.MockConcertoService{} + ds, err := NewLabelService(cs) + assert.Nil(err, "Couldn't load label service") + assert.NotNil(ds, "Label service not instanced") + + // to json + dIn, err := json.Marshal(labelIn) + assert.Nil(err, "Label test data corrupted") + + // call service + resourceID := "5b5074735f7c880ad9c6bbce" + cs.On("Delete", fmt.Sprintf("v1/labels/%s/resources/%s/%s", labelIn.ID, labelIn.ResourceType, resourceID)).Return(dIn, 404, nil) + err = ds.RemoveLabel(labelIn.ID, labelIn.ResourceType, resourceID) + + assert.NotNil(err, "We are expecting an status code error") + assert.Contains(err.Error(), "404", "Error should contain http code 404") +} diff --git a/api/labels/labels_api_test.go b/api/labels/labels_api_test.go new file mode 100644 index 0000000..f14e715 --- /dev/null +++ b/api/labels/labels_api_test.go @@ -0,0 +1,54 @@ +package labels + +import ( + "testing" + + "github.com/ingrammicro/concerto/testdata" + "github.com/stretchr/testify/assert" +) + +func TestNewLabelServiceNil(t *testing.T) { + assert := assert.New(t) + rs, err := NewLabelService(nil) + assert.Nil(rs, "Uninitialized service should return nil") + assert.NotNil(err, "Uninitialized service should return error") +} + +func TestGetLabelsList(t *testing.T) { + labelsIn := testdata.GetLabelData() + GetLabelListMocked(t, labelsIn) + GetLabelListMockedWithNamespace(t, testdata.GetLabelWithNamespaceData()) + GetLabelListFailErrMocked(t, labelsIn) + GetLabelListFailStatusMocked(t, labelsIn) + GetLabelListFailJSONMocked(t, labelsIn) +} + +func TestCreateLabel(t *testing.T) { + labelsIn := testdata.GetLabelData() + for _, labelIn := range *labelsIn { + CreateLabelMocked(t, &labelIn) + CreateLabelFailErrMocked(t, &labelIn) + CreateLabelFailStatusMocked(t, &labelIn) + CreateLabelFailJSONMocked(t, &labelIn) + } +} + +func TestAddLabel(t *testing.T) { + labelsIn := testdata.GetLabelData() + labeledResourcesOut := testdata.GetLabeledResourcesData() + for _, labelIn := range *labelsIn { + AddLabelMocked(t, &labelIn, *labeledResourcesOut) + AddLabelFailErrMocked(t, &labelIn, *labeledResourcesOut) + AddLabelFailStatusMocked(t, &labelIn, *labeledResourcesOut) + AddLabelFailJSONMocked(t, &labelIn, *labeledResourcesOut) + } +} + +func TestRemoveLabel(t *testing.T) { + labelsIn := testdata.GetLabelData() + for _, labelIn := range *labelsIn { + RemoveLabelMocked(t, &labelIn) + RemoveLabelFailErrMocked(t, &labelIn) + RemoveLabelFailStatusMocked(t, &labelIn) + } +} diff --git a/api/types/bootstrapping.go b/api/types/bootstrapping.go new file mode 100644 index 0000000..1fee663 --- /dev/null +++ b/api/types/bootstrapping.go @@ -0,0 +1,28 @@ +package types + +import ( + "encoding/json" +) + +type BootstrappingConfiguration struct { + Policyfiles []BootstrappingPolicyfile `json:"policyfiles,omitempty" header:"POLICY FILES" show:"nolist"` + Attributes *json.RawMessage `json:"attributes,omitempty" header:"ATTRIBUTES" show:"nolist"` + AttributeRevisionID string `json:"attribute_revision_id,omitempty" header:"ATTRIBUTE REVISION ID"` +} + +type BootstrappingPolicyfile struct { + ID string `json:"id,omitempty" header:"ID"` + RevisionID string `json:"revision_id,omitempty" header:"REVISION ID"` + DownloadURL string `json:"download_url,omitempty" header:"DOWNLOAD URL"` +} + +type BootstrappingContinuousReport struct { + Stdout string `json:"stdout" header:"STDOUT"` +} + +type BootstrappingAppliedConfiguration struct { + StartedAt string `json:"started_at,omitempty" header:"STARTED AT"` + FinishedAt string `json:"finished_at,omitempty" header:"FINISHED AT"` + PolicyfileRevisionIDs string `json:"policyfile_revision_ids,omitempty" header:"POLICY FILE REVISION IDS" show:"nolist"` + AttributeRevisionID string `json:"attribute_revision_id,omitempty" header:"ATTRIBUTE REVISION ID"` +} diff --git a/api/types/firewall_profiles.go b/api/types/firewall_profiles.go index ef771d6..8ef416e 100644 --- a/api/types/firewall_profiles.go +++ b/api/types/firewall_profiles.go @@ -1,11 +1,13 @@ package types type FirewallProfile struct { - ID string `json:"id" header:"ID"` - Name string `json:"name,omitempty" header:"NAME"` - Description string `json:"description,omitempty" header:"DESCRIPTION"` - Default bool `json:"default,omitempty" header:"DEFAULT"` - Rules []Rule `json:"rules,omitempty" header:"RULES" show:"nolist"` + ID string `json:"id" header:"ID"` + Name string `json:"name,omitempty" header:"NAME"` + Description string `json:"description,omitempty" header:"DESCRIPTION"` + Default bool `json:"default,omitempty" header:"DEFAULT"` + Rules []Rule `json:"rules,omitempty" header:"RULES" show:"nolist"` + ResourceType string `json:"resource_type" header:"RESOURCE_TYPE" show:"nolist"` + LabelableFields } type Rule struct { diff --git a/api/types/labels.go b/api/types/labels.go new file mode 100644 index 0000000..a90a952 --- /dev/null +++ b/api/types/labels.go @@ -0,0 +1,61 @@ +package types + +type Label struct { + ID string `json:"id" header:"ID"` + Name string `json:"name" header:"NAME"` + ResourceType string `json:"resource_type" header:"RESOURCE_TYPE"` + Namespace string `json:"namespace" header:"NAMESPACE" show:"nolist"` + Value string `json:"value" header:"VALUE" show:"nolist"` +} + +type LabeledResource struct { + ID string `json:"id" header:"ID"` + ResourceType string `json:"resource_type" header:"RESOURCE_TYPE"` +} + +type LabelableFields struct { + LabelIDs []string `json:"label_ids" header:"LABEL_IDS" show:"nolist,noshow"` + Labels []string `json:"labels" header:"LABELS"` +} + +type Labelable interface { + FilterByLabelIDs(labelIDs []string) bool + AssignLabelIDs(labelIDs []string) + FillInLabelNames(labelNamesByID map[string]string) +} + +func (lf *LabelableFields) FilterByLabelIDs(labelIDs []string) bool { + for _, lid := range labelIDs { + var labelIDFound bool + for _, resourceLabelID := range lf.LabelIDs { + if lid == resourceLabelID { + labelIDFound = true + break + } + } + if !labelIDFound { + return false + } + } + return true +} + +func (lf *LabelableFields) AssignLabelIDs(labelIDs []string) { + for _, lid := range labelIDs { + for _, resourceLabelID := range lf.LabelIDs { + if lid == resourceLabelID { + break + } + } + } +} + +func (lf *LabelableFields) FillInLabelNames(labelNamesByID map[string]string) { + for lID, lName := range labelNamesByID { + for _, resourceLabelID := range lf.LabelIDs { + if lID == resourceLabelID { + lf.Labels = append(lf.Labels, lName) + } + } + } +} diff --git a/api/types/scripts.go b/api/types/scripts.go index 3f1b011..36d127a 100644 --- a/api/types/scripts.go +++ b/api/types/scripts.go @@ -2,9 +2,11 @@ package types // Script holds script data type Script struct { - ID string `json:"id" header:"ID"` - Name string `json:"name" header:"NAME"` - Description string `json:"description" header:"DESCRIPTION"` - Code string `json:"code" header:"CODE" show:"nolist"` - Parameters []string `json:"parameters" header:"PARAMETERS"` + ID string `json:"id" header:"ID"` + Name string `json:"name" header:"NAME"` + Description string `json:"description" header:"DESCRIPTION"` + Code string `json:"code" header:"CODE" show:"nolist"` + Parameters []string `json:"parameters" header:"PARAMETERS"` + ResourceType string `json:"resource_type" header:"RESOURCE_TYPE" show:"nolist"` + LabelableFields } diff --git a/api/types/servers.go b/api/types/servers.go index e752b6b..dcab057 100644 --- a/api/types/servers.go +++ b/api/types/servers.go @@ -11,6 +11,8 @@ type Server struct { CloudAccountID string `json:"cloud_account_id" header:"CLOUD_ACCOUNT_ID"` SSHProfileID string `json:"ssh_profile_id" header:"SSH_PROFILE_ID"` FirewallProfileID string `json:"firewall_profile_id" header:"FIREWALL_PROFILE_ID"` + ResourceType string `json:"resource_type" header:"RESOURCE_TYPE" show:"nolist"` + LabelableFields } type Dns struct { diff --git a/api/types/ssh_profiles.go b/api/types/ssh_profiles.go index ef9c3cb..ccd8162 100644 --- a/api/types/ssh_profiles.go +++ b/api/types/ssh_profiles.go @@ -1,8 +1,10 @@ package types type SSHProfile struct { - ID string `json:"id" header:"ID"` - Name string `json:"name" heade:"NAME"` - PublicKey string `json:"public_key" header:"PUBLIC_KEY"` - PrivateKey string `json:"private_key" header:"PRIVATE_KEY"` + ID string `json:"id" header:"ID"` + Name string `json:"name" header:"NAME"` + PublicKey string `json:"public_key" header:"PUBLIC_KEY"` + PrivateKey string `json:"private_key" header:"PRIVATE_KEY" show:"nolist"` + ResourceType string `json:"resource_type" header:"RESOURCE_TYPE" show:"nolist"` + LabelableFields } diff --git a/api/types/templates.go b/api/types/templates.go index 52ecfec..dc484cf 100644 --- a/api/types/templates.go +++ b/api/types/templates.go @@ -11,6 +11,8 @@ type Template struct { GenericImageID string `json:"generic_image_id,omitempty" header:"GENERIC IMAGE ID"` ServiceList []string `json:"service_list,omitempty" header:"SERVICE LIST" show:"nolist"` ConfigurationAttributes *json.RawMessage `json:"configuration_attributes,omitempty" header:"CONFIGURATION ATTRIBUTES" show:"nolist"` + ResourceType string `json:"resource_type" header:"RESOURCE_TYPE" show:"nolist"` + LabelableFields } // TemplateScript stores a templates' script info diff --git a/blueprint/scripts/subcommands.go b/blueprint/scripts/subcommands.go index f635a09..93984d6 100644 --- a/blueprint/scripts/subcommands.go +++ b/blueprint/scripts/subcommands.go @@ -11,6 +11,12 @@ func SubCommands() []cli.Command { Name: "list", Usage: "Lists all available scripts", Action: cmd.ScriptsList, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label as a query filter", + }, + }, }, { Name: "show", @@ -44,6 +50,10 @@ func SubCommands() []cli.Command { Name: "parameters", Usage: "The names of the script's parameters", }, + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label names to be associated with script", + }, }, }, { @@ -84,5 +94,47 @@ func SubCommands() []cli.Command { }, }, }, + { + Name: "add-label", + Usage: "This action assigns a single label from a single labelable resource", + Action: cmd.LabelAdd, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Script Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "script", + Hidden: true, + }, + }, + }, + { + Name: "remove-label", + Usage: "This action unassigns a single label from a single labelable resource", + Action: cmd.LabelRemove, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Script Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "script", + Hidden: true, + }, + }, + }, } } diff --git a/blueprint/templates/subcommands.go b/blueprint/templates/subcommands.go index d3c632e..557cd3a 100644 --- a/blueprint/templates/subcommands.go +++ b/blueprint/templates/subcommands.go @@ -11,6 +11,12 @@ func SubCommands() []cli.Command { Name: "list", Usage: "Lists all available templates", Action: cmd.TemplateList, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label as a query filter", + }, + }, }, { Name: "show", @@ -44,6 +50,10 @@ func SubCommands() []cli.Command { Name: "configuration_attributes", Usage: "The attributes used to configure the services in the service_list", }, + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label names to be associated with template", + }, }, }, { @@ -201,5 +211,47 @@ func SubCommands() []cli.Command { }, }, }, + { + Name: "add-label", + Usage: "This action assigns a single label from a single labelable resource", + Action: cmd.LabelAdd, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Template Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "template", + Hidden: true, + }, + }, + }, + { + Name: "remove-label", + Usage: "This action unassigns a single label from a single labelable resource", + Action: cmd.LabelRemove, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Template Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "template", + Hidden: true, + }, + }, + }, } } diff --git a/bootstrapping/bootstrapping.go b/bootstrapping/bootstrapping.go new file mode 100644 index 0000000..b730555 --- /dev/null +++ b/bootstrapping/bootstrapping.go @@ -0,0 +1,427 @@ +package bootstrapping + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "net/url" + "os" + "os/signal" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + log "github.com/Sirupsen/logrus" + singleinstance "github.com/allan-simon/go-singleinstance" + "github.com/codegangsta/cli" + "github.com/ingrammicro/concerto/api/blueprint" + "github.com/ingrammicro/concerto/api/types" + "github.com/ingrammicro/concerto/cmd" + "github.com/ingrammicro/concerto/utils" + "github.com/ingrammicro/concerto/utils/format" +) + +const ( + //DefaultTimingInterval Default period for looping + DefaultTimingInterval = 600 // 600 seconds = 10 minutes + DefaultTimingSplay = 360 // seconds + DefaultThresholdLines = 10 + ProcessLockFile = "cio-bootstrapping.lock" + RetriesNumber = 5 +) + +type bootstrappingProcess struct { + startedAt time.Time + finishedAt time.Time + policyfiles []policyfile + attributes attributes + thresholdLines int + directoryPath string + appliedPolicyfileRevisionIDs map[string]string +} +type attributes struct { + revisionID string + rawData *json.RawMessage +} + +type policyfile types.BootstrappingPolicyfile + +func (pf policyfile) Name() string { + return strings.Join([]string{pf.ID, "-", pf.RevisionID}, "") +} + +func (pf *policyfile) FileName() string { + return strings.Join([]string{pf.Name(), "tgz"}, ".") +} + +func (pf *policyfile) QueryURL() (string, error) { + if pf.DownloadURL == "" { + return "", fmt.Errorf("obtaining URL query: empty download URL") + } + url, err := url.Parse(pf.DownloadURL) + if err != nil { + return "", fmt.Errorf("parsing URL to extract query: %v", err) + } + return fmt.Sprintf("%s?%s", url.Path, url.RawQuery), nil +} + +func (pf *policyfile) TarballPath(dir string) string { + return filepath.Join(dir, pf.FileName()) +} + +func (pf *policyfile) Path(dir string) string { + return filepath.Join(dir, pf.Name()) +} + +func (a *attributes) FileName() string { + return fmt.Sprintf("attrs-%s.json", a.revisionID) +} + +func (a *attributes) FilePath(dir string) string { + return filepath.Join(dir, a.FileName()) +} + +// Handle signals +func handleSysSignals(cancelFunc context.CancelFunc) { + log.Debug("handleSysSignals") + + gracefulStop := make(chan os.Signal, 1) + signal.Notify(gracefulStop, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL) + log.Debug("Ending, signal detected:", <-gracefulStop) + cancelFunc() +} + +// Returns the full path to the tmp directory joined with pid management file name +func lockFilePath() string { + return filepath.Join(os.TempDir(), ProcessLockFile) +} + +func workspaceDir() string { + return filepath.Join(os.TempDir(), "cio") +} + +// Returns the full path to the tmp directory +func generateWorkspaceDir() error { + dir := workspaceDir() + dirInfo, err := os.Stat(dir) + if err != nil { + err := os.Mkdir(dir, 0777) + if err != nil { + return err + } + } else { + if !dirInfo.Mode().IsDir() { + return fmt.Errorf("%s exists but is not a directory", dir) + } + } + return nil +} + +// Start the bootstrapping process +func start(c *cli.Context) error { + log.Debug("start") + + err := generateWorkspaceDir() + if err != nil { + return err + } + lockFile, err := singleinstance.CreateLockFile(lockFilePath()) + if err != nil { + return err + } + defer lockFile.Close() + + formatter := format.GetFormatter() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go handleSysSignals(cancel) + + timingInterval := c.Int64("interval") + if !(timingInterval > 0) { + timingInterval = DefaultTimingInterval + } + + timingSplay := c.Int64("splay") + if !(timingSplay > 0) { + timingSplay = DefaultTimingSplay + } + + thresholdLines := c.Int("lines") + if !(thresholdLines > 0) { + thresholdLines = DefaultThresholdLines + } + log.Debug("routine lines threshold: ", thresholdLines) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + bootstrappingSvc, formatter := cmd.WireUpBootstrapping(c) + for { + applyPolicyfiles(ctx, bootstrappingSvc, formatter, thresholdLines) + + // Sleep for a configured amount of time plus a random amount of time (10 minutes plus 0 to 5 minutes, for instance) + ticker := time.NewTicker(time.Duration(timingInterval+int64(r.Intn(int(timingSplay)))) * time.Second) + + select { + case <-ticker.C: + log.Debug("ticker") + case <-ctx.Done(): + log.Debug(ctx.Err()) + log.Debug("closing bootstrapping") + } + ticker.Stop() + if ctx.Err() != nil { + break + } + } + + return nil +} + +// Stop the bootstrapping process +func stop(c *cli.Context) error { + log.Debug("cmdStop") + + formatter := format.GetFormatter() + if err := utils.StopProcess(lockFilePath()); err != nil { + formatter.PrintFatal("cannot stop the bootstrapping process", err) + } + + log.Info("Bootstrapping routine successfully stopped") + return nil +} + +// Subsidiary routine for commands processing +func applyPolicyfiles(ctx context.Context, bootstrappingSvc *blueprint.BootstrappingService, formatter format.Formatter, thresholdLines int) error { + log.Debug("applyPolicyfiles") + + // Inquire about desired configuration changes to be applied by querying the `GET /blueprint/configuration` endpoint. This will provide a JSON response with the desired configuration changes + bsConfiguration, status, err := bootstrappingSvc.GetBootstrappingConfiguration() + if err == nil && status != 200 { + err = fmt.Errorf("received non-ok %d response", status) + } + if err != nil { + formatter.PrintError("couldn't receive bootstrapping data", err) + return err + } + err = generateWorkspaceDir() + if err != nil { + formatter.PrintError("couldn't generated workspace directory", err) + return err + } + bsProcess := &bootstrappingProcess{ + startedAt: time.Now().UTC(), + thresholdLines: thresholdLines, + directoryPath: workspaceDir(), + appliedPolicyfileRevisionIDs: make(map[string]string), + } + + // proto structures + err = initializePrototype(bsConfiguration, bsProcess) + if err != nil { + formatter.PrintError("couldn't initialize prototype", err) + return err + } + // For every policyfile, ensure its tarball (downloadable through their download_url) has been downloaded to the server ... + err = downloadPolicyfiles(ctx, bootstrappingSvc, bsProcess) + if err != nil { + formatter.PrintError("couldn't download policy files", err) + return err + } + //... and clean off any tarball that is no longer needed. + err = cleanObsoletePolicyfiles(bsProcess) + if err != nil { + formatter.PrintError("couldn't clean obsolete policy files", err) + return err + } + // Store the attributes as JSON in a file with name `attrs-.json` + err = saveAttributes(bsProcess) + if err != nil { + formatter.PrintError("couldn't save attributes for policy files", err) + return err + } + // Process tarballs policies + err = processPolicyfiles(bootstrappingSvc, bsProcess) + // Finishing time + bsProcess.finishedAt = time.Now().UTC() + + // Inform the platform of applied changes via a `PUT /blueprint/applied_configuration` request with a JSON payload similar to + log.Debug("reporting applied policy files") + reportErr := reportAppliedConfiguration(bootstrappingSvc, bsProcess) + if reportErr != nil { + formatter.PrintError("couldn't report applied status for policy files", err) + return err + } + return err +} + +func initializePrototype(bsConfiguration *types.BootstrappingConfiguration, bsProcess *bootstrappingProcess) error { + log.Debug("initializePrototype") + + // Attributes + bsProcess.attributes.revisionID = bsConfiguration.AttributeRevisionID + bsProcess.attributes.rawData = bsConfiguration.Attributes + + // Policies + for _, bsConfPolicyfile := range bsConfiguration.Policyfiles { + bsProcess.policyfiles = append(bsProcess.policyfiles, policyfile(bsConfPolicyfile)) + } + log.Debug(bsProcess) + return nil +} + +// downloadPolicyfiles For every policy file, ensure its tarball (downloadable through their download_url) has been downloaded to the server ... +func downloadPolicyfiles(ctx context.Context, bootstrappingSvc *blueprint.BootstrappingService, bsProcess *bootstrappingProcess) error { + log.Debug("downloadPolicyfiles") + + for _, bsPolicyfile := range bsProcess.policyfiles { + tarballPath := bsPolicyfile.TarballPath(bsProcess.directoryPath) + log.Debug("downloading: ", tarballPath) + queryURL, err := bsPolicyfile.QueryURL() + if err != nil { + return err + } + _, status, err := bootstrappingSvc.DownloadPolicyfile(queryURL, tarballPath) + if err == nil && status != 200 { + err = fmt.Errorf("obtained non-ok response when downloading policyfile %s", queryURL) + } + if err != nil { + return err + } + if err = utils.Untar(ctx, tarballPath, bsPolicyfile.Path(bsProcess.directoryPath)); err != nil { + return err + } + } + return nil +} + +// cleanObsoletePolicyfiles cleans off any tarball that is no longer needed. +func cleanObsoletePolicyfiles(bsProcess *bootstrappingProcess) error { + log.Debug("cleanObsoletePolicyfiles") + + // evaluates working folder + deletableFiles, err := ioutil.ReadDir(bsProcess.directoryPath) + if err != nil { + return err + } + + // builds an array of currently processable files at this looping time + currentlyProcessableFiles := []string{bsProcess.attributes.FileName()} // saved attributes file name + for _, bsPolicyFile := range bsProcess.policyfiles { + currentlyProcessableFiles = append(currentlyProcessableFiles, bsPolicyFile.FileName()) // Downloaded tgz file names + currentlyProcessableFiles = append(currentlyProcessableFiles, bsPolicyFile.Name()) // Uncompressed folder names + } + + // removes from deletableFiles array the policy files currently applied + for _, f := range deletableFiles { + if !utils.Contains(currentlyProcessableFiles, f.Name()) { + log.Debug("removing: ", f.Name()) + if err := os.RemoveAll(filepath.Join(bsProcess.directoryPath, f.Name())); err != nil { + return err + } + } + } + return nil +} + +// saveAttributes stores the attributes as JSON in a file with name `attrs-.json` +func saveAttributes(bsProcess *bootstrappingProcess) error { + log.Debug("saveAttributes") + + attrs, err := json.Marshal(bsProcess.attributes.rawData) + if err != nil { + return err + } + if err := ioutil.WriteFile(bsProcess.attributes.FilePath(bsProcess.directoryPath), attrs, 0600); err != nil { + return err + } + return nil +} + +// processPolicyfiles applies for each policy the required chef commands, reporting in bunches of N lines +func processPolicyfiles(bootstrappingSvc *blueprint.BootstrappingService, bsProcess *bootstrappingProcess) error { + log.Debug("processPolicyfiles") + + for _, bsPolicyfile := range bsProcess.policyfiles { + command := fmt.Sprintf("chef-client -z -j %s", bsProcess.attributes.FilePath(bsProcess.directoryPath)) + policyfileDir := bsPolicyfile.Path(bsProcess.directoryPath) + var renamedPolicyfileDir string + if runtime.GOOS == "windows" { + renamedPolicyfileDir = policyfileDir + policyfileDir = filepath.Join(bsProcess.directoryPath, "active") + err := os.Rename(renamedPolicyfileDir, policyfileDir) + if err != nil { + return fmt.Errorf("could not rename %s as %s: %v", renamedPolicyfileDir, policyfileDir, err) + } + command = fmt.Sprintf("SET \"PATH=%%PATH%%;C:\\ruby\\bin;C:\\opscode\\chef\\bin;C:\\opscode\\chef\\embedded\\bin\"\n%s", command) + } + command = fmt.Sprintf("cd %s\n%s", policyfileDir, command) + + log.Debug(command) + + // Custom method for chunks processing + fn := func(chunk string) error { + log.Debug("sendChunks") + err := utils.Retry(RetriesNumber, time.Second, func() error { + log.Debug("Sending: ", chunk) + + commandIn := map[string]interface{}{ + "stdout": chunk, + } + + _, statusCode, err := bootstrappingSvc.ReportBootstrappingLog(&commandIn) + switch { + // 0<100 error cases?? + case statusCode == 0: + return fmt.Errorf("communication error %v %v", statusCode, err) + case statusCode >= 500: + return fmt.Errorf("server error %v %v", statusCode, err) + case statusCode >= 400: + return fmt.Errorf("client error %v %v", statusCode, err) + default: + return nil + } + }) + + if err != nil { + return fmt.Errorf("cannot send the chunk data, %v", err) + } + return nil + } + + exitCode, err := utils.RunContinuousCmd(fn, command, -1, bsProcess.thresholdLines) + if err == nil && exitCode != 0 { + err = fmt.Errorf("policyfile application exited with %d code", exitCode) + } + if err != nil { + return err + } + + log.Info("completed: ", exitCode) + bsProcess.appliedPolicyfileRevisionIDs[bsPolicyfile.ID] = bsPolicyfile.RevisionID + if renamedPolicyfileDir != "" { + err = os.Rename(policyfileDir, renamedPolicyfileDir) + if err != nil { + return fmt.Errorf("could not rename %s as %s back: %v", policyfileDir, renamedPolicyfileDir, err) + } + } + } + return nil +} + +// reportAppliedConfiguration Inform the platform of applied changes +func reportAppliedConfiguration(bootstrappingSvc *blueprint.BootstrappingService, bsProcess *bootstrappingProcess) error { + log.Debug("reportAppliedConfiguration") + + payload := map[string]interface{}{ + "started_at": bsProcess.startedAt, + "finished_at": bsProcess.finishedAt, + "policyfile_revision_ids": bsProcess.appliedPolicyfileRevisionIDs, + "attribute_revision_id": bsProcess.attributes.revisionID, + } + return bootstrappingSvc.ReportBootstrappingAppliedConfiguration(&payload) +} diff --git a/bootstrapping/subcommands.go b/bootstrapping/subcommands.go new file mode 100644 index 0000000..119ab29 --- /dev/null +++ b/bootstrapping/subcommands.go @@ -0,0 +1,37 @@ +package bootstrapping + +import ( + "github.com/codegangsta/cli" +) + +func SubCommands() []cli.Command { + return []cli.Command{ + { + Name: "start", + Usage: "Starts a bootstrapping routine to check and execute required activities", + Action: start, + Flags: []cli.Flag{ + cli.Int64Flag{ + Name: "interval, i", + Usage: "The frequency (in seconds) at which the bootstrapping runs", + Value: DefaultTimingInterval, + }, + cli.Int64Flag{ + Name: "splay, s", + Usage: "A random number between zero and splay that is added to interval (seconds)", + Value: DefaultTimingSplay, + }, + cli.IntFlag{ + Name: "lines, l", + Usage: "Maximum lines threshold per response chunk", + Value: DefaultThresholdLines, + }, + }, + }, + { + Name: "stop", + Usage: "Stops the running bootstrapping process", + Action: stop, + }, + } +} diff --git a/cloud/servers/subcommands.go b/cloud/servers/subcommands.go index aba1889..4c9ac7d 100644 --- a/cloud/servers/subcommands.go +++ b/cloud/servers/subcommands.go @@ -11,6 +11,12 @@ func SubCommands() []cli.Command { Name: "list", Usage: "Lists information about all the servers on this account.", Action: cmd.ServerList, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label as a query filter", + }, + }, }, { Name: "show", @@ -52,6 +58,10 @@ func SubCommands() []cli.Command { Name: "cloud_account_id", Usage: "Identifier of the cloud account in which the server shall be registered", }, + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label names to be associated with server", + }, }, }, { @@ -162,5 +172,47 @@ func SubCommands() []cli.Command { }, }, }, + { + Name: "add-label", + Usage: "This action assigns a single label from a single labelable resource", + Action: cmd.LabelAdd, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Server Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "server", + Hidden: true, + }, + }, + }, + { + Name: "remove-label", + Usage: "This action unassigns a single label from a single labelable resource", + Action: cmd.LabelRemove, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Server Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "server", + Hidden: true, + }, + }, + }, } } diff --git a/cloud/ssh_profiles/subcommands.go b/cloud/ssh_profiles/subcommands.go index ef5af9f..2122135 100644 --- a/cloud/ssh_profiles/subcommands.go +++ b/cloud/ssh_profiles/subcommands.go @@ -11,6 +11,12 @@ func SubCommands() []cli.Command { Name: "list", Usage: "Lists all available SSH profiles.", Action: cmd.SSHProfileList, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label as a query filter", + }, + }, }, { Name: "show", @@ -40,6 +46,10 @@ func SubCommands() []cli.Command { Name: "private_key", Usage: "Private key of the SSH profile", }, + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label names to be associated with SSH profile", + }, }, }, { @@ -76,5 +86,47 @@ func SubCommands() []cli.Command { }, }, }, + { + Name: "add-label", + Usage: "This action assigns a single label from a single labelable resource", + Action: cmd.LabelAdd, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "SSH profile id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "ssh_profile", + Hidden: true, + }, + }, + }, + { + Name: "remove-label", + Usage: "This action unassigns a single label from a single labelable resource", + Action: cmd.LabelRemove, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "SSH profile id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "ssh_profile", + Hidden: true, + }, + }, + }, } } diff --git a/cmd/bootstrapping_cmd.go b/cmd/bootstrapping_cmd.go new file mode 100644 index 0000000..9394b3f --- /dev/null +++ b/cmd/bootstrapping_cmd.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "github.com/codegangsta/cli" + "github.com/ingrammicro/concerto/api/blueprint" + "github.com/ingrammicro/concerto/utils" + "github.com/ingrammicro/concerto/utils/format" +) + +// WireUpBootstrapping prepares common resources to send request to API +func WireUpBootstrapping(c *cli.Context) (ds *blueprint.BootstrappingService, f format.Formatter) { + + f = format.GetFormatter() + + config, err := utils.GetConcertoConfig() + if err != nil { + f.PrintFatal("Couldn't wire up config", err) + } + hcs, err := utils.NewHTTPConcertoService(config) + if err != nil { + f.PrintFatal("Couldn't wire up concerto service", err) + } + ds, err = blueprint.NewBootstrappingService(hcs) + if err != nil { + f.PrintFatal("Couldn't wire up serverPlan service", err) + } + + return ds, f +} \ No newline at end of file diff --git a/cmd/firewall_profiles_cmd.go b/cmd/firewall_profiles_cmd.go index 94e65f0..4c30d9f 100644 --- a/cmd/firewall_profiles_cmd.go +++ b/cmd/firewall_profiles_cmd.go @@ -1,8 +1,11 @@ package cmd import ( + "fmt" + "github.com/codegangsta/cli" "github.com/ingrammicro/concerto/api/network" + "github.com/ingrammicro/concerto/api/types" "github.com/ingrammicro/concerto/utils" "github.com/ingrammicro/concerto/utils/format" ) @@ -37,6 +40,23 @@ func FirewallProfileList(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive firewallProfile data", err) } + + labelables := make([]types.Labelable, len(firewallProfiles)) + for i:=0; i< len(firewallProfiles); i++ { + labelables[i] = types.Labelable(&firewallProfiles[i]) + } + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + filteredLabelables := LabelFiltering(c, labelables, labelIDsByName) + LabelAssignNamesForIDs(c, filteredLabelables, labelNamesByID) + firewallProfiles = make([]types.FirewallProfile, len(filteredLabelables)) + for i, labelable := range filteredLabelables { + fw, ok := labelable.(*types.FirewallProfile) + if !ok { + formatter.PrintFatal("Label filtering returned unexpected result", + fmt.Errorf("expected labelable to be a *types.FirewallProfile, got a %T", labelable)) + } + firewallProfiles[i] = *fw + } if err = formatter.PrintList(firewallProfiles); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -53,6 +73,8 @@ func FirewallProfileShow(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive firewallProfile data", err) } + _, labelNamesByID := LabelLoadsMapping(c) + firewallProfile.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*firewallProfile); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -69,10 +91,28 @@ func FirewallProfileCreate(c *cli.Context) error { if err != nil { formatter.PrintFatal("Error parsing parameters", err) } - firewallProfile, err := firewallProfileSvc.CreateFirewallProfile(params) + + firewallProfileIn := map[string]interface{}{ + "name": c.String("name"), + "description": c.String("description"), + } + if c.String("rules") != "" { + firewallProfileIn["rules"] = (*params)["rules"] + } + + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + + if c.IsSet("labels") { + labelsIdsArr := LabelResolution(c, c.String("labels"), labelIDsByName) + firewallProfileIn["label_ids"] = labelsIdsArr + } + + firewallProfile, err := firewallProfileSvc.CreateFirewallProfile(&firewallProfileIn) if err != nil { formatter.PrintFatal("Couldn't create firewallProfile", err) } + + firewallProfile.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*firewallProfile); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -93,6 +133,9 @@ func FirewallProfileUpdate(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't update firewallProfile", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + firewallProfile.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*firewallProfile); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } diff --git a/cmd/labels_cmd.go b/cmd/labels_cmd.go new file mode 100644 index 0000000..95a68ac --- /dev/null +++ b/cmd/labels_cmd.go @@ -0,0 +1,216 @@ +package cmd + +import ( + "fmt" + "regexp" + "strings" + + "github.com/codegangsta/cli" + "github.com/ingrammicro/concerto/api/labels" + "github.com/ingrammicro/concerto/api/types" + "github.com/ingrammicro/concerto/utils" + "github.com/ingrammicro/concerto/utils/format" +) + +// WireUpLabel prepares common resources to send request to Concerto API +func WireUpLabel(c *cli.Context) (ds *labels.LabelService, f format.Formatter) { + + f = format.GetFormatter() + + config, err := utils.GetConcertoConfig() + if err != nil { + f.PrintFatal("Couldn't wire up config", err) + } + hcs, err := utils.NewHTTPConcertoService(config) + if err != nil { + f.PrintFatal("Couldn't wire up concerto service", err) + } + ds, err = labels.NewLabelService(hcs) + if err != nil { + f.PrintFatal("Couldn't wire up label service", err) + } + + return ds, f +} + +// LabelList subcommand function +func LabelList(c *cli.Context) error { + debugCmdFuncInfo(c) + + labelsSvc, formatter := WireUpLabel(c) + labels, err := labelsSvc.GetLabelList() + if err != nil { + formatter.PrintFatal("Couldn't receive labels data", err) + } + + if err = formatter.PrintList(labels); err != nil { + formatter.PrintFatal("Couldn't print/format result", err) + } + return nil +} + +// LabelCreate subcommand function +func LabelCreate(c *cli.Context) error { + debugCmdFuncInfo(c) + + labelsSvc, formatter := WireUpLabel(c) + checkRequiredFlags(c, []string{"name", "resource_type"}, formatter) + label, err := labelsSvc.CreateLabel(utils.FlagConvertParams(c)) + if err != nil { + formatter.PrintFatal("Couldn't create label", err) + } + if err = formatter.PrintItem(*label); err != nil { + formatter.PrintFatal("Couldn't print/format result", err) + } + return nil +} + +// LabelFiltering subcommand function receives a collection of references to labelable objects +// Evaluates the matching of assigned labels with the labels requested for filtering. +func LabelFiltering(c *cli.Context, items []types.Labelable, labelIDsByName map[string]string) []types.Labelable { + debugCmdFuncInfo(c) + + if c.String("labels") != "" { + _, formatter := WireUpLabel(c) + labelNamesIn := LabelsUnifyInputNames(c.String("labels"), formatter) + var filteringLabelIDs []string + for _, name := range labelNamesIn { + id := labelIDsByName[name] + filteringLabelIDs = append(filteringLabelIDs, id) + } + var result []types.Labelable + for _, item := range items { + if item.FilterByLabelIDs(filteringLabelIDs) { + result = append(result, item) + } + } + return result + } + + return items +} + +// LabelAssignNamesForIDs subcommand function receives a collection of references to labelables objects +// Resolves the Labels names associated to a each resource from given Labels ids, loading object with respective labels names +func LabelAssignNamesForIDs(c *cli.Context, items []types.Labelable, labelNamesByID map[string]string) { + debugCmdFuncInfo(c) + for _, labelable := range items { + labelable.FillInLabelNames(labelNamesByID) + } +} + +// LabelLoadsMapping subcommand function retrieves the current label list in IMCO; then prepares two mapping structures (Name <-> ID and ID <-> Name) +func LabelLoadsMapping(c *cli.Context) (map[string]string, map[string]string) { + debugCmdFuncInfo(c) + + labelsSvc, formatter := WireUpLabel(c) + labels, err := labelsSvc.GetLabelList() + if err != nil { + formatter.PrintFatal("Couldn't receive labels data", err) + } + + labelIDsByName := make(map[string]string) + labelNamesByID := make(map[string]string) + + for _, label := range labels { + labelIDsByName[label.Name] = label.ID + labelNamesByID[label.ID] = label.Name + } + return labelIDsByName, labelNamesByID +} + +// LabelsUnifyInputNames subcommand function evaluates the received labels names (comma separated string). +// Validates, remove duplicates and resolves a slice with unique label names. +func LabelsUnifyInputNames(labelsNames string, formatter format.Formatter) []string { + labelNamesIn := utils.RemoveDuplicates(strings.Split(labelsNames, ",")) + for _, c := range labelNamesIn { + matched := regexp.MustCompile(`^[A-Za-z0-9 .\s_-]+$`).MatchString(c) + if !matched { + formatter.PrintFatal("Invalid label name ", fmt.Errorf("Invalid label format: %v (Labels would be indicated with their name, which must satisfy to be composed of spaces, underscores, dots, dashes and/or lower/upper -case alphanumeric characters-)", c)) + } + } + return labelNamesIn +} + +// LabelResolution subcommand function retrieves a labels map(Name<->ID) based on label names received to be processed. +// The function evaluates the received labels names (comma separated string); with them, solves the assigned IDs for the given labels names. +// If the label name is not available in IMCO yet, it is created. +func LabelResolution(c *cli.Context, labelsNames string, labelIDsByName map[string]string) []string { + debugCmdFuncInfo(c) + + labelsSvc, formatter := WireUpLabel(c) + labelNamesIn := LabelsUnifyInputNames(labelsNames, formatter) + + // Obtain output mapped labels Name<->ID; currently in IMCO platform as well as if creation is required + labelsOutMap := make(map[string]string) + for _, name := range labelNamesIn { + // check if the label already exists in IMCO, creates it if it does not exist + if labelIDsByName[name] == "" { + labelPayload := make(map[string]interface{}) + labelPayload["name"] = name + newLabel, err := labelsSvc.CreateLabel(&labelPayload) + if err != nil { + formatter.PrintFatal("Couldn't create label", err) + } + labelsOutMap[name] = newLabel.ID + } else { + labelsOutMap[name] = labelIDsByName[name] + } + } + labelsIdsArr := make([]string, 0) + for _, mp := range labelsOutMap { + labelsIdsArr = append(labelsIdsArr, mp) + } + return labelsIdsArr +} + +// LabelAdd subcommand function assigns a single label from a single labelable resource +func LabelAdd(c *cli.Context) error { + debugCmdFuncInfo(c) + + labelsSvc, formatter := WireUpLabel(c) + checkRequiredFlags(c, []string{"id", "label"}, formatter) + + labelIDsByName, _ := LabelLoadsMapping(c) + labelsIdsArr := LabelResolution(c, c.String("label"), labelIDsByName) + if len(labelsIdsArr) > 1 { + formatter.PrintFatal("Too many label names. Please, Use only one label name", fmt.Errorf("Invalid parameter: %v - %v", c.String("label"), labelsIdsArr)) + } + labelID := labelsIdsArr[0] + + resData := make(map[string]string) + resData["id"] = c.String("id") + resData["resource_type"] = c.String("resource_type") + resourcesData := make([]interface{}, 0, 1) + resourcesData = append(resourcesData, resData) + + labelIn := map[string]interface{}{ + "resources": resourcesData, + } + + labeledResources, err := labelsSvc.AddLabel(&labelIn, labelID) + if err != nil { + formatter.PrintFatal("Couldn't add label data", err) + } + if err = formatter.PrintList(labeledResources); err != nil { + formatter.PrintFatal("Couldn't print/format result", err) + } + return nil +} + +// LabelRemove subcommand function de-assigns a single label from a single labelable resource +func LabelRemove(c *cli.Context) error { + debugCmdFuncInfo(c) + + labelsSvc, formatter := WireUpLabel(c) + checkRequiredFlags(c, []string{"id", "label"}, formatter) + + labelsMapNameToID, _ := LabelLoadsMapping(c) + labelID := labelsMapNameToID[c.String("label")] + + err := labelsSvc.RemoveLabel(labelID, c.String("resource_type"), c.String("id")) + if err != nil { + formatter.PrintFatal("Couldn't remove label", err) + } + return nil +} diff --git a/cmd/scripts_cmd.go b/cmd/scripts_cmd.go index 2568f03..0e1386e 100644 --- a/cmd/scripts_cmd.go +++ b/cmd/scripts_cmd.go @@ -1,8 +1,12 @@ package cmd import ( + "fmt" + "strings" + "github.com/codegangsta/cli" "github.com/ingrammicro/concerto/api/blueprint" + "github.com/ingrammicro/concerto/api/types" "github.com/ingrammicro/concerto/utils" "github.com/ingrammicro/concerto/utils/format" ) @@ -37,6 +41,26 @@ func ScriptsList(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive script data", err) } + + labelables := make([]types.Labelable, len(scripts)) + for i:=0; i< len(scripts); i++ { + labelables[i] = types.Labelable(&scripts[i]) + } + + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + filteredLabelables := LabelFiltering(c, labelables, labelIDsByName) + LabelAssignNamesForIDs(c, filteredLabelables, labelNamesByID) + + scripts = make([]types.Script, len(filteredLabelables)) + for i, labelable := range filteredLabelables { + s, ok := labelable.(*types.Script) + if !ok { + formatter.PrintFatal("Label filtering returned unexpected result", + fmt.Errorf("expected labelable to be a *types.Script, got a %T", labelable)) + } + scripts[i] = *s + } + if err = formatter.PrintList(scripts); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -53,6 +77,9 @@ func ScriptShow(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive script data", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + script.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*script); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -65,10 +92,28 @@ func ScriptCreate(c *cli.Context) error { scriptSvc, formatter := WireUpScript(c) checkRequiredFlags(c, []string{"name", "description", "code"}, formatter) - script, err := scriptSvc.CreateScript(utils.FlagConvertParams(c)) + scriptIn := map[string]interface{}{ + "name": c.String("name"), + "description": c.String("description"), + "code": c.String("code"), + } + if c.String("parameters") != "" { + scriptIn["parameters"] = strings.Split(c.String("parameters"), ",") + } + + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + + if c.IsSet("labels") { + labelsIdsArr := LabelResolution(c, c.String("labels"), labelIDsByName) + scriptIn["label_ids"] = labelsIdsArr + } + + script, err := scriptSvc.CreateScript(&scriptIn) if err != nil { formatter.PrintFatal("Couldn't create script", err) } + + script.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*script); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -85,6 +130,9 @@ func ScriptUpdate(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't update script", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + script.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*script); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } diff --git a/cmd/servers_cmd.go b/cmd/servers_cmd.go index 5041e8f..64267ec 100644 --- a/cmd/servers_cmd.go +++ b/cmd/servers_cmd.go @@ -1,8 +1,10 @@ package cmd import ( + "fmt" "github.com/codegangsta/cli" "github.com/ingrammicro/concerto/api/cloud" + "github.com/ingrammicro/concerto/api/types" "github.com/ingrammicro/concerto/utils" "github.com/ingrammicro/concerto/utils/format" ) @@ -37,6 +39,23 @@ func ServerList(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive server data", err) } + + labelables := make([]types.Labelable, len(servers)) + for i:=0; i< len(servers); i++ { + labelables[i] = types.Labelable(&servers[i]) + } + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + filteredLabelables := LabelFiltering(c, labelables, labelIDsByName) + LabelAssignNamesForIDs(c, filteredLabelables, labelNamesByID) + servers = make([]types.Server, len(filteredLabelables)) + for i, labelable := range filteredLabelables { + s, ok := labelable.(*types.Server) + if !ok { + formatter.PrintFatal("Label filtering returned unexpected result", + fmt.Errorf("expected labelable to be a *types.Server, got a %T", labelable)) + } + servers[i] = *s + } if err = formatter.PrintList(servers); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -53,6 +72,9 @@ func ServerShow(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive server data", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + server.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*server); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -65,10 +87,28 @@ func ServerCreate(c *cli.Context) error { serverSvc, formatter := WireUpServer(c) checkRequiredFlags(c, []string{"name", "ssh_profile_id", "firewall_profile_id", "template_id", "server_plan_id", "cloud_account_id"}, formatter) - server, err := serverSvc.CreateServer(utils.FlagConvertParams(c)) + serverIn := map[string]interface{}{ + "name": c.String("name"), + "ssh_profile_id": c.String("ssh_profile_id"), + "firewall_profile_id": c.String("firewall_profile_id"), + "template_id": c.String("template_id"), + "server_plan_id": c.String("server_plan_id"), + "cloud_account_id": c.String("cloud_account_id"), + } + + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + + if c.IsSet("labels") { + labelsIdsArr := LabelResolution(c, c.String("labels"), labelIDsByName) + serverIn["label_ids"] = labelsIdsArr + } + + server, err := serverSvc.CreateServer(&serverIn) if err != nil { formatter.PrintFatal("Couldn't create server", err) } + + server.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*server); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -85,6 +125,9 @@ func ServerUpdate(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't update server", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + server.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*server); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -101,6 +144,9 @@ func ServerBoot(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't boot server", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + server.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*server); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -117,6 +163,9 @@ func ServerReboot(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't reboot server", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + server.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*server); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -133,6 +182,9 @@ func ServerShutdown(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't shutdown server", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + server.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*server); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -149,6 +201,9 @@ func ServerOverride(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't override server", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + server.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*server); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } diff --git a/cmd/ssh_profiles_cmd.go b/cmd/ssh_profiles_cmd.go index 4010b45..a3fb548 100644 --- a/cmd/ssh_profiles_cmd.go +++ b/cmd/ssh_profiles_cmd.go @@ -1,8 +1,11 @@ package cmd import ( + "fmt" + "github.com/codegangsta/cli" "github.com/ingrammicro/concerto/api/cloud" + "github.com/ingrammicro/concerto/api/types" "github.com/ingrammicro/concerto/utils" "github.com/ingrammicro/concerto/utils/format" ) @@ -37,6 +40,24 @@ func SSHProfileList(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive sshProfile data", err) } + + labelables := make([]types.Labelable, len(sshProfiles)) + for i:=0; i< len(sshProfiles); i++ { + labelables[i] = types.Labelable(&sshProfiles[i]) + } + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + filteredLabelables := LabelFiltering(c, labelables, labelIDsByName) + LabelAssignNamesForIDs(c, filteredLabelables, labelNamesByID) + sshProfiles = make([]types.SSHProfile, len(filteredLabelables)) + for i, labelable := range filteredLabelables { + sshP, ok := labelable.(*types.SSHProfile) + if !ok { + formatter.PrintFatal("Label filtering returned unexpected result", + fmt.Errorf("expected labelable to be a *types.SSHProfile, got a %T", labelable)) + } + sshProfiles[i] = *sshP + } + if err = formatter.PrintList(sshProfiles); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -53,6 +74,8 @@ func SSHProfileShow(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive sshProfile data", err) } + _, labelNamesByID := LabelLoadsMapping(c) + sshProfile.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*sshProfile); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -65,10 +88,27 @@ func SSHProfileCreate(c *cli.Context) error { sshProfileSvc, formatter := WireUpSSHProfile(c) checkRequiredFlags(c, []string{"name", "public_key"}, formatter) - sshProfile, err := sshProfileSvc.CreateSSHProfile(utils.FlagConvertParams(c)) + sshProfileIn := map[string]interface{}{ + "name": c.String("name"), + "public_key": c.String("public_key"), + } + if c.String("private_key") != "" { + sshProfileIn["private_key"] = c.String("private_key") + } + + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + + if c.IsSet("labels") { + labelsIdsArr := LabelResolution(c, c.String("labels"), labelIDsByName) + sshProfileIn["label_ids"] = labelsIdsArr + } + + sshProfile, err := sshProfileSvc.CreateSSHProfile(&sshProfileIn) if err != nil { formatter.PrintFatal("Couldn't create sshProfile", err) } + + sshProfile.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*sshProfile); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -85,6 +125,9 @@ func SSHProfileUpdate(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't update sshProfile", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + sshProfile.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*sshProfile); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } diff --git a/cmd/template_cmd.go b/cmd/template_cmd.go index 9ef3ccd..9c39cdf 100644 --- a/cmd/template_cmd.go +++ b/cmd/template_cmd.go @@ -1,8 +1,11 @@ package cmd import ( + "fmt" + "github.com/codegangsta/cli" "github.com/ingrammicro/concerto/api/blueprint" + "github.com/ingrammicro/concerto/api/types" "github.com/ingrammicro/concerto/utils" "github.com/ingrammicro/concerto/utils/format" ) @@ -37,6 +40,25 @@ func TemplateList(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive template data", err) } + + labelables := make([]types.Labelable, len(templates)) + for i:=0; i< len(templates); i++ { + labelables[i] = types.Labelable(&templates[i]) + } + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + filteredLabelables := LabelFiltering(c, labelables, labelIDsByName) + LabelAssignNamesForIDs(c, filteredLabelables, labelNamesByID) + + templates = make([]types.Template, len(filteredLabelables)) + for i, labelable := range filteredLabelables { + tpl, ok := labelable.(*types.Template) + if !ok { + formatter.PrintFatal("Label filtering returned unexpected result", + fmt.Errorf("expected labelable to be a *types.Template, got a %T", labelable)) + } + templates[i] = *tpl + } + if err = formatter.PrintList(templates); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -53,6 +75,9 @@ func TemplateShow(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't receive template data", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + template.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*template); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -65,17 +90,32 @@ func TemplateCreate(c *cli.Context) error { templateSvc, formatter := WireUpTemplate(c) checkRequiredFlags(c, []string{"name", "generic_image_id"}, formatter) - // parse json parameter values params, err := utils.FlagConvertParamsJSON(c, []string{"service_list", "configuration_attributes"}) if err != nil { formatter.PrintFatal("Error parsing parameters", err) } - template, err := templateSvc.CreateTemplate(params) + templateIn := map[string]interface{}{ + "name": c.String("name"), + "generic_image_id": c.String("generic_image_id"), + "service_list": (*params)["service_list"], + "configuration_attributes": (*params)["configuration_attributes"], + } + + labelIDsByName, labelNamesByID := LabelLoadsMapping(c) + + if c.IsSet("labels") { + labelsIdsArr := LabelResolution(c, c.String("labels"), labelIDsByName) + templateIn["label_ids"] = labelsIdsArr + } + + template, err := templateSvc.CreateTemplate(&templateIn) if err != nil { formatter.PrintFatal("Couldn't create template", err) } + + template.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*template); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } @@ -99,6 +139,9 @@ func TemplateUpdate(c *cli.Context) error { if err != nil { formatter.PrintFatal("Couldn't update template", err) } + + _, labelNamesByID := LabelLoadsMapping(c) + template.FillInLabelNames(labelNamesByID) if err = formatter.PrintItem(*template); err != nil { formatter.PrintFatal("Couldn't print/format result", err) } diff --git a/cmdpolling/continuousreport.go b/cmdpolling/continuousreport.go index 6183388..568314f 100644 --- a/cmdpolling/continuousreport.go +++ b/cmdpolling/continuousreport.go @@ -15,7 +15,6 @@ import ( const ( RetriesNumber = 5 - RetriesFactor = 3 DefaultThresholdTime = 10 ) @@ -43,7 +42,7 @@ func cmdContinuousReportRun(c *cli.Context) error { // Custom method for chunks processing fn := func(chunk string) error { log.Debug("sendChunks") - err := retry(RetriesNumber, time.Second, func() error { + err := utils.Retry(RetriesNumber, time.Second, func() error { log.Debug("Sending: ", chunk) commandIn := map[string]interface{}{ @@ -70,7 +69,7 @@ func cmdContinuousReportRun(c *cli.Context) error { return nil } - exitCode, err := utils.RunContinuousCmd(fn, cmdArg, thresholdTime) + exitCode, err := utils.RunContinuousCmd(fn, cmdArg, thresholdTime, -1) if err != nil { formatter.PrintFatal("cannot process continuous report command", err) } @@ -79,17 +78,3 @@ func cmdContinuousReportRun(c *cli.Context) error { os.Exit(exitCode) return nil } - -func retry(attempts int, sleep time.Duration, fn func() error) error { - log.Debug("retry") - - if err := fn(); err != nil { - if attempts--; attempts > 0 { - log.Debug("Waiting to retry: ", sleep) - time.Sleep(sleep) - return retry(attempts, RetriesFactor*sleep, fn) - } - return err - } - return nil -} diff --git a/cmdpolling/polling.go b/cmdpolling/polling.go index 07f6274..c97990b 100644 --- a/cmdpolling/polling.go +++ b/cmdpolling/polling.go @@ -19,7 +19,7 @@ import ( const ( DefaultPollingPingTimingIntervalLong = 30 DefaultPollingPingTimingIntervalShort = 5 - ProcessIdFile = "imco-polling.pid" + ProcessIdFile = "cio-polling.pid" ) // Handle signals diff --git a/docs/images/commissioned-server.png b/docs/images/commissioned-server.png index e2a9730..667ab9f 100644 Binary files a/docs/images/commissioned-server.png and b/docs/images/commissioned-server.png differ diff --git a/docs/images/server-bootstraping.png b/docs/images/server-bootstraping.png index 2e7bbe5..4466016 100644 Binary files a/docs/images/server-bootstraping.png and b/docs/images/server-bootstraping.png differ diff --git a/docs/images/server-operational.png b/docs/images/server-operational.png index cb7e35b..b5dddf8 100644 Binary files a/docs/images/server-operational.png and b/docs/images/server-operational.png differ diff --git a/labels/subcommands.go b/labels/subcommands.go new file mode 100644 index 0000000..e86c699 --- /dev/null +++ b/labels/subcommands.go @@ -0,0 +1,16 @@ +package labels + +import ( + "github.com/codegangsta/cli" + "github.com/ingrammicro/concerto/cmd" +) + +func SubCommands() []cli.Command { + return []cli.Command{ + { + Name: "list", + Usage: "Lists the current labels existing in the platform for the user", + Action: cmd.LabelList, + }, + } +} diff --git a/main.go b/main.go index bbe80ff..fb8383e 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + //"context" "fmt" "os" "sort" @@ -11,6 +12,7 @@ import ( "github.com/ingrammicro/concerto/blueprint/scripts" "github.com/ingrammicro/concerto/blueprint/services" "github.com/ingrammicro/concerto/blueprint/templates" + "github.com/ingrammicro/concerto/bootstrapping" "github.com/ingrammicro/concerto/brownfield" cl_prov "github.com/ingrammicro/concerto/cloud/cloud_providers" "github.com/ingrammicro/concerto/cloud/generic_images" @@ -22,6 +24,7 @@ import ( "github.com/ingrammicro/concerto/converge" "github.com/ingrammicro/concerto/dispatcher" "github.com/ingrammicro/concerto/firewall" + "github.com/ingrammicro/concerto/labels" "github.com/ingrammicro/concerto/network/firewall_profiles" "github.com/ingrammicro/concerto/settings/cloud_accounts" "github.com/ingrammicro/concerto/setup" @@ -67,6 +70,13 @@ var ServerCommands = []cli.Command{ cmdpolling.SubCommands(), ), }, + { + Name: "bootstrap", + Usage: "Manages bootstrapping commands", + Subcommands: append( + bootstrapping.SubCommands(), + ), + }, } var BlueprintCommands = []cli.Command{ @@ -247,6 +257,14 @@ var ClientCommands = []cli.Command{ WizardCommands, ), }, + { + Name: "labels", + ShortName: "lbl", + Usage: "Provides information about labels", + Subcommands: append( + labels.SubCommands(), + ), + }, } var appFlags = []cli.Flag{ diff --git a/network/firewall_profiles/subcommands.go b/network/firewall_profiles/subcommands.go index 23ff6e1..4e03f34 100644 --- a/network/firewall_profiles/subcommands.go +++ b/network/firewall_profiles/subcommands.go @@ -11,6 +11,12 @@ func SubCommands() []cli.Command { Name: "list", Usage: "Lists all existing firewall profiles", Action: cmd.FirewallProfileList, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label as a query filter", + }, + }, }, { Name: "show", @@ -40,6 +46,10 @@ func SubCommands() []cli.Command { Name: "rules", Usage: "Set of rules of the firewall profile", }, + cli.StringFlag{ + Name: "labels", + Usage: "A list of comma separated label names to be associated with firewall profile", + }, }, }, { @@ -76,5 +86,47 @@ func SubCommands() []cli.Command { }, }, }, + { + Name: "add-label", + Usage: "This action assigns a single label from a single labelable resource", + Action: cmd.LabelAdd, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Firewall profile Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "firewall_profile", + Hidden: true, + }, + }, + }, + { + Name: "remove-label", + Usage: "This action unassigns a single label from a single labelable resource", + Action: cmd.LabelRemove, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "Firewall profile Id", + }, + cli.StringFlag{ + Name: "label", + Usage: "Label name", + }, + cli.StringFlag{ + Name: "resource_type", + Usage: "Resource Type", + Value: "firewall_profile", + Hidden: true, + }, + }, + }, } } diff --git a/testdata/boostrapping_data.go b/testdata/boostrapping_data.go new file mode 100644 index 0000000..5467995 --- /dev/null +++ b/testdata/boostrapping_data.go @@ -0,0 +1,48 @@ +package testdata + +import ( + "encoding/json" + "github.com/ingrammicro/concerto/api/types" +) + +// GetBootstrappingConfigurationData loads test data +func GetBootstrappingConfigurationData() *types.BootstrappingConfiguration { + + attrs := json.RawMessage(`{"fakeAttribute0":"val0","fakeAttribute1":"val1"}`) + test := types.BootstrappingConfiguration{ + Policyfiles: []types.BootstrappingPolicyfile{ + { + ID: "fakeProfileID0", + RevisionID: "fakeProfileRevisionID0", + DownloadURL: "fakeProfileDownloadURL0", + }, + { + ID: "fakeProfileID1", + RevisionID: "fakeProfileRevisionID1", + DownloadURL: "fakeProfileDownloadURL1", + }, + }, + Attributes: &attrs, + AttributeRevisionID: "fakeAttributeRevisionID", + } + + return &test +} + +// GetBootstrappingContinuousReportData loads test data +func GetBootstrappingContinuousReportData() *types.BootstrappingContinuousReport { + + testBootstrappingContinuousReport := types.BootstrappingContinuousReport{ + Stdout: "Bootstrap log created", + } + + return &testBootstrappingContinuousReport +} + +//GetBootstrappingDownloadFileData +func GetBootstrappingDownloadFileData() map[string]string { + return map[string]string{ + "fakeURLToFile": "http://fakeURLToFile.xxx/filename.tgz", + "fakeFileDownloadFile": "filename.tgz", + } +} diff --git a/testdata/labels_data.go b/testdata/labels_data.go new file mode 100644 index 0000000..9597576 --- /dev/null +++ b/testdata/labels_data.go @@ -0,0 +1,64 @@ +package testdata + +import ( + "github.com/ingrammicro/concerto/api/types" +) + +// GetLabelData loads test data +func GetLabelData() *[]types.Label { + + testLabels := []types.Label{ + { + ID: "fakeID0", + Name: "fakeName0", + ResourceType: "label", + }, + { + ID: "fakeID1", + Name: "fakeName1", + ResourceType: "label", + }, + } + + return &testLabels +} + +// GetLabelWithNamespaceData loads test data +func GetLabelWithNamespaceData() *[]types.Label { + + testLabels := []types.Label{ + { + ID: "fakeID0", + Name: "fakeName0", + ResourceType: "label", + Namespace: "fakeNamespace0", + Value: "fakeValue0", + }, + { + ID: "fakeID1", + Name: "fakeName1", + ResourceType: "label", + Namespace: "fakeNamespace1", + Value: "fakeValue1", + }, + } + + return &testLabels +} + +// GetLabeledResourcesData loads test data +func GetLabeledResourcesData() *[]types.LabeledResource { + + testLabeledResources := []types.LabeledResource{ + { + ID: "fakeID0", + ResourceType: "server", + }, + { + ID: "fakeID1", + ResourceType: "template", + }, + } + + return &testLabeledResources +} diff --git a/utils/config.go b/utils/config.go index ae8fd8f..9523b52 100644 --- a/utils/config.go +++ b/utils/config.go @@ -20,18 +20,18 @@ import ( "github.com/mitchellh/go-homedir" ) -const windowsServerConfigFile = "c:\\imco\\client.xml" -const nixServerConfigFile = "/etc/imco/client.xml" +const windowsServerConfigFile = "c:\\cio\\client.xml" +const nixServerConfigFile = "/etc/cio/client.xml" const defaultConcertoEndpoint = "https://clients.concerto.io:886/" -const windowsServerLogFilePath = "c:\\imco\\log\\concerto-client.log" -const windowsServerCaCertPath = "c:\\imco\\client_ssl\\ca_cert.pem" -const windowsServerCertPath = "c:\\imco\\client_ssl\\cert.pem" -const windowsServerKeyPath = "c:\\imco\\client_ssl\\private\\key.pem" +const windowsServerLogFilePath = "c:\\cio\\log\\concerto-client.log" +const windowsServerCaCertPath = "c:\\cio\\client_ssl\\ca_cert.pem" +const windowsServerCertPath = "c:\\cio\\client_ssl\\cert.pem" +const windowsServerKeyPath = "c:\\cio\\client_ssl\\private\\key.pem" const nixServerLogFilePath = "/var/log/concerto-client.log" -const nixServerCaCertPath = "/etc/imco/client_ssl/ca_cert.pem" -const nixServerCertPath = "/etc/imco/client_ssl/cert.pem" -const nixServerKeyPath = "/etc/imco/client_ssl/private/key.pem" +const nixServerCaCertPath = "/etc/cio/client_ssl/ca_cert.pem" +const nixServerCertPath = "/etc/cio/client_ssl/cert.pem" +const nixServerKeyPath = "/etc/cio/client_ssl/private/key.pem" // Config stores configuration file contents type Config struct { diff --git a/utils/exec.go b/utils/exec.go index 7268bee..f16fedc 100644 --- a/utils/exec.go +++ b/utils/exec.go @@ -19,6 +19,7 @@ import ( const ( TimeStampLayout = "2006-01-02T15:04:05.000000-07:00" TimeLayoutYYYYMMDDHHMMSS = "20060102150405" + RetriesFactor = 3 ) func extractExitCode(err error) int { @@ -234,7 +235,9 @@ func RunTracedCmd(command string) (exitCode int, stdOut string, stdErr string, s return } -func RunContinuousCmd(fn func(chunk string) error, command string, thresholdTime int) (int, error) { +// thresholdTime > 0 continuous report +// thresholdLines > 0 bootstrapping +func RunContinuousCmd(fn func(chunk string) error, command string, thresholdTime int, thresholdLines int) (int, error) { log.Debug("RunContinuousCmd") // Saves script/command in a temp file @@ -256,20 +259,19 @@ func RunContinuousCmd(fn func(chunk string) error, command string, thresholdTime } chunk := "" - nTime := 0 + nLines, nTime := 0, 0 timeStart := time.Now() scanner := bufio.NewScanner(bufio.NewReader(stdout)) for scanner.Scan() { chunk = strings.Join([]string{chunk, scanner.Text(), "\n"}, "") + nLines++ nTime = int(time.Now().Sub(timeStart).Seconds()) - if nTime >= thresholdTime { - if err := fn(chunk); err != nil { - nTime = 0 - } else { + if (thresholdTime > 0 && nTime >= thresholdTime) || (thresholdLines > 0 && nLines >= thresholdLines) { + if err := fn(chunk); err == nil { chunk = "" - nTime = 0 } + nLines, nTime = 0, 0 timeStart = time.Now() } } @@ -291,3 +293,17 @@ func RunContinuousCmd(fn func(chunk string) error, command string, thresholdTime return exitCode, nil } + +func Retry(attempts int, sleep time.Duration, fn func() error) error { + log.Debug("Retry") + + if err := fn(); err != nil { + if attempts--; attempts > 0 { + log.Debug("Waiting to retry: ", sleep) + time.Sleep(sleep) + return Retry(attempts, RetriesFactor*sleep, fn) + } + return err + } + return nil +} diff --git a/utils/format/formatter.go b/utils/format/formatter.go index 0ebbb6f..869f4e8 100644 --- a/utils/format/formatter.go +++ b/utils/format/formatter.go @@ -16,7 +16,6 @@ var osExit = os.Exit type Formatter interface { PrintItem(item interface{}) error PrintList(items interface{}) error - //PrintList(items [][]string, headers []string) error PrintError(context string, err error) PrintFatal(context string, err error) } @@ -24,8 +23,8 @@ type Formatter interface { var formatter Formatter // InitializeFormatter creates a singleton Formatter -func InitializeFormatter(ftype string, out io.Writer) { - if ftype == "json" { +func InitializeFormatter(formatterType string, out io.Writer) { + if formatterType == "json" { formatter = NewJSONFormatter(out) } else { formatter = NewTextFormatter(out) diff --git a/utils/format/textformatter.go b/utils/format/textformatter.go index 3408de7..3acbe43 100644 --- a/utils/format/textformatter.go +++ b/utils/format/textformatter.go @@ -2,6 +2,7 @@ package format import ( "fmt" + "github.com/ingrammicro/concerto/utils" "io" "reflect" "strings" @@ -10,8 +11,6 @@ import ( log "github.com/Sirupsen/logrus" ) -const minifySeconds string = "minifySeconds" - // TextFormatter prints items and lists type TextFormatter struct { output io.Writer @@ -26,31 +25,85 @@ func NewTextFormatter(out io.Writer) *TextFormatter { } } -// PrintItem prints an item -func (f *TextFormatter) PrintItem(item interface{}) error { - log.Debug("PrintItem") +func (f *TextFormatter) printItemAux(w *tabwriter.Writer, item interface{}) error { + log.Debug("printItemAux") it := reflect.ValueOf(item) - nf := it.NumField() - - w := tabwriter.NewWriter(f.output, 15, 1, 3, ' ', 0) - for i := 0; i < nf; i++ { - // TODO not the best way to use reflection. Check this later - switch it.Field(i).Type().String() { - case "json.RawMessage": - fmt.Fprintf(w, "%s:\t%s\n", it.Type().Field(i).Tag.Get("header"), it.Field(i).Interface()) - case "*json.RawMessage": - fmt.Fprintf(w, "%s:\t%s\n", it.Type().Field(i).Tag.Get("header"), it.Field(i).Elem()) - default: - fmt.Fprintf(w, "%s:\t%+v\n", it.Type().Field(i).Tag.Get("header"), it.Field(i).Interface()) + for i := 0; i < it.NumField(); i++ { + showTags := strings.Split(it.Type().Field(i).Tag.Get("show"), ",") + if !utils.Contains(showTags, "noshow") { + switch it.Field(i).Type().String() { + case "json.RawMessage": + fmt.Fprintln(w, fmt.Sprintf("%s:\t%s", it.Type().Field(i).Tag.Get("header"), it.Field(i).Interface())) + case "*json.RawMessage": + fmt.Fprintln(w, fmt.Sprintf("%s:\t%s", it.Type().Field(i).Tag.Get("header"), it.Field(i).Elem())) + default: + if it.Field(i).Kind() == reflect.Struct { + f.printItemAux(w, it.Field(i).Interface()) + } else { + fmt.Fprintln(w, fmt.Sprintf("%s:\t%+v", it.Type().Field(i).Tag.Get("header"), it.Field(i).Interface())) + } + } } } - fmt.Fprintln(w) + return nil +} + +// PrintItem prints item +func (f *TextFormatter) PrintItem(item interface{}) error { + log.Debug("PrintItem") + + w := tabwriter.NewWriter(f.output, 15, 1, 3, ' ', 0) + f.printItemAux(w, item) w.Flush() return nil } +func (f *TextFormatter) printListHeadersAux(w *tabwriter.Writer, t reflect.Type) { + log.Debug("printListHeadersAux") + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if field.Type.Kind() == reflect.Struct { + f.printListHeadersAux(w, field.Type) + } + + showTags := strings.Split(field.Tag.Get("show"), ",") + if !utils.Contains(showTags, "nolist") { + fmt.Fprint(w, fmt.Sprintf("%+v\t", field.Tag.Get("header"))) + } + } +} + +func (f *TextFormatter) printListBodyAux(w *tabwriter.Writer, t reflect.Value) { + log.Debug("printListBodyAux") + + for i := 0; i < t.NumField(); i++ { + showTags := strings.Split(t.Type().Field(i).Tag.Get("show"), ",") + if !utils.Contains(showTags, "nolist") { + field := t.Field(i) + switch field.Type().String() { + case "json.RawMessage": + fmt.Fprint(w, fmt.Sprintf("%s\t", field.Interface())) + case "*json.RawMessage": + if field.IsNil() { + fmt.Fprint(w, fmt.Sprintf(" \t")) + } else { + fmt.Fprint(w, fmt.Sprintf("%s\t", field.Elem())) + } + default: + if field.Kind() == reflect.Struct { + f.printListBodyAux(w, field) + } else { + fmt.Fprint(w, fmt.Sprintf("%+v\t", field.Interface())) + } + } + } + } +} + // PrintList prints item list func (f *TextFormatter) PrintList(items interface{}) error { log.Debug("PrintList") @@ -59,82 +112,21 @@ func (f *TextFormatter) PrintList(items interface{}) error { its := reflect.ValueOf(items) t := its.Type().Kind() if t != reflect.Slice { - return fmt.Errorf("Couldn't print list. Expected slice, but received %s", t.String()) + return fmt.Errorf("couldn't print list. Expected slice, but received %s", t.String()) } w := tabwriter.NewWriter(f.output, 15, 1, 3, ' ', 0) - header := reflect.TypeOf(items).Elem() - nf := header.NumField() - - // avoid printing elements with 'show:nolist' attribute - // special format tags - avoid := make([]bool, nf) - format := make([]string, nf) - for i := 0; i < nf; i++ { - avoid[i] = false - showTags := strings.Split(header.Field(i).Tag.Get("show"), ",") - for _, showTag := range showTags { - if showTag == "nolist" { - avoid[i] = true - } - if showTag == minifySeconds { - format[i] = minifySeconds - } - } - } - - // print header - for i := 0; i < nf; i++ { - if !avoid[i] { - fmt.Fprintf(w, "%+v\t", header.Field(i).Tag.Get("header")) - } - } + // Headers + f.printListHeadersAux(w, reflect.TypeOf(items).Elem()) fmt.Fprintln(w) - // print contents - for i := 0; i < its.Len(); i++ { - it := its.Index(i) - nf := it.NumField() - for i := 0; i < nf; i++ { - if !avoid[i] { - - if format[i] == minifySeconds { - - remainingSeconds := int(it.Field(i).Float()) - s := remainingSeconds % 60 - remainingSeconds = (remainingSeconds - s) - m := int(remainingSeconds/60) % 60 - remainingSeconds = (remainingSeconds - m*60) - h := (remainingSeconds / 3600) % 24 - remainingSeconds = (remainingSeconds - h*3600) - d := int(remainingSeconds / 86400) - - if d > 0 { - fmt.Fprintf(w, "%dd%dh%dm\t", d, h, m) - } else { - fmt.Fprintf(w, "%dh%dm%ds\t", h, m, s) - } - - } else { - - switch it.Field(i).Type().String() { - case "json.RawMessage": - fmt.Fprintf(w, "%s\t", it.Field(i).Interface()) - case "*json.RawMessage": - if it.Field(i).IsNil() { - fmt.Fprintf(w, " \t") - } else { - fmt.Fprintf(w, "%s\t", it.Field(i).Elem()) - } - default: - fmt.Fprintf(w, "%+v\t", it.Field(i).Interface()) - } - } - } - } + // Body + for pos := 0; pos < its.Len(); pos++ { + f.printListBodyAux(w, its.Index(pos)) fmt.Fprintln(w) } + w.Flush() return nil diff --git a/utils/utils.go b/utils/utils.go index d4de652..f67b5c6 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,12 +2,15 @@ package utils import ( "archive/zip" + "context" "fmt" "io" "math/rand" "os" + "os/exec" "path/filepath" "regexp" + "runtime" "strings" "time" @@ -60,6 +63,24 @@ func Unzip(archive, target string) error { return nil } +func Untar(ctx context.Context, source, target string) error { + + if err := os.MkdirAll(target, 0600); err != nil { + return err + } + + tarExecutable := "tar" + if runtime.GOOS == "windows" { + tarExecutable = "C:\\opscode\\chef\\bin\\tar.exe" + } + cmd := exec.CommandContext(ctx, tarExecutable, "-xzf", source, "-C", target) + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + func ScrapeErrorMessage(message string, regExpression string) string { re, err := regexp.Compile(regExpression) @@ -167,6 +188,7 @@ func CheckRequiredFlags(c *cli.Context, flags []string) { } } +// RandomString generates a random string from lowercase letters and numbers func RandomString(strlen int) string { r := rand.New(rand.NewSource(time.Now().UnixNano())) const chars = "abcdefghijklmnopqrstuvwxyz0123456789" @@ -176,3 +198,69 @@ func RandomString(strlen int) string { } return string(result) } + +// RemoveDuplicates returns the slice removing duplicates if exist +func RemoveDuplicates(elements []string) []string { + encountered := map[string]bool{} + + // Create a map of all unique elements. + for v := range elements { + encountered[elements[v]] = true + } + + // Place all keys from the map into a slice. + result := []string{} + for key := range encountered { + result = append(result, key) + } + return result +} + +// Contains evaluates whether s contains x. +func Contains(s []string, x string) bool { + for _, n := range s { + if x == n { + return true + } + } + return false +} + +// Subset returns true if the first slice is completely contained in the second slice. +// There must be at least the same number of duplicate values in second as there are in first. +func Subset(s1, s2 []string) bool { + if len(s1) > len(s2) { + return false + } + for _, e := range s1 { + if !Contains(s2, e) { + return false + } + } + return true +} + +func RemoveFileInfo(fileInfo os.FileInfo, fileInfoName string) error { + if fileInfo.IsDir() { + d, err := os.Open(fileInfoName) + if err != nil { + return err + } + defer d.Close() + names, err := d.Readdirnames(-1) + if err != nil { + return err + } + for _, name := range names { + err = os.RemoveAll(filepath.Join(fileInfoName, name)) + if err != nil { + return err + } + } + } + + if err := os.Remove(fileInfoName); err != nil { + return err + } + return nil +} diff --git a/utils/version.go b/utils/version.go index efe8ba3..6ef1669 100644 --- a/utils/version.go +++ b/utils/version.go @@ -1,3 +1,3 @@ package utils -const VERSION = "0.7.0" +const VERSION = "0.8.0" diff --git a/utils/webservice.go b/utils/webservice.go index c356184..6e2952d 100644 --- a/utils/webservice.go +++ b/utils/webservice.go @@ -8,7 +8,6 @@ import ( "io/ioutil" "net/http" "os" - "regexp" "strings" log "github.com/Sirupsen/logrus" @@ -20,7 +19,7 @@ type ConcertoService interface { Put(path string, payload *map[string]interface{}) ([]byte, int, error) Delete(path string) ([]byte, int, error) Get(path string) ([]byte, int, error) - GetFile(path string, directoryPath string) (string, int, error) + GetFile(path string, filePath string) (string, int, error) } // HTTPConcertoservice web service manager. @@ -198,7 +197,7 @@ func (hcs *HTTPConcertoservice) Get(path string) ([]byte, int, error) { } // GetFile sends GET request to Concerto API and receives a file -func (hcs *HTTPConcertoservice) GetFile(path string, directoryPath string) (string, int, error) { +func (hcs *HTTPConcertoservice) GetFile(path string, filePath string) (string, int, error) { url, _, err := hcs.prepareCall(path, nil) if err != nil { @@ -214,14 +213,7 @@ func (hcs *HTTPConcertoservice) GetFile(path string, directoryPath string) (stri defer response.Body.Close() log.Debugf("Status code:%d message:%s", response.StatusCode, response.Status) - r, err := regexp.Compile("filename=\\\"([^\\\"]*){1}\\\"") - if err != nil { - return "", response.StatusCode, err - } - - // TODO check errors - fileName := r.FindStringSubmatch(response.Header.Get("Content-Disposition"))[1] - realFileName := fmt.Sprintf("%s/%s", directoryPath, fileName) + realFileName := filePath output, err := os.Create(realFileName) if err != nil { diff --git a/utils/webservice_mock.go b/utils/webservice_mock.go index a70f810..75dd0bc 100644 --- a/utils/webservice_mock.go +++ b/utils/webservice_mock.go @@ -34,7 +34,7 @@ func (m *MockConcertoService) Get(path string) ([]byte, int, error) { } // GetFile sends GET request to Concerto API and receives a file -func (m *MockConcertoService) GetFile(path string, directoryPath string) (string, int, error) { - args := m.Called(path, directoryPath) +func (m *MockConcertoService) GetFile(path string, filePath string) (string, int, error) { + args := m.Called(path, filePath) return args.String(0), args.Int(1), args.Error(2) }