├── demo-project ├── src │ └── myproject │ │ └── hello.clj ├── .gitignore ├── project.clj └── templates │ └── cloudformation │ └── infrastructure.clj ├── .gitignore ├── src └── crucible │ ├── aws │ ├── ssm.clj │ ├── sqs.clj │ ├── serverless │ │ ├── function │ │ │ ├── event_source │ │ │ │ ├── schedule.clj │ │ │ │ ├── sns.clj │ │ │ │ ├── sqs.clj │ │ │ │ ├── api.clj │ │ │ │ └── kinesis.clj │ │ │ ├── dead_letter_queue.clj │ │ │ └── event_source.clj │ │ ├── simple_table.clj │ │ ├── layer_version.clj │ │ ├── api.clj │ │ ├── globals.clj │ │ └── function.clj │ ├── ecr.clj │ ├── ecs │ │ ├── cluster.clj │ │ ├── secret.clj │ │ ├── key_value_pair.clj │ │ ├── task_definition.clj │ │ └── service.clj │ ├── route53.clj │ ├── kms │ │ ├── alias.clj │ │ └── key.clj │ ├── kms.clj │ ├── ec2 │ │ ├── route_table.clj │ │ ├── subnet_route_table_association.clj │ │ ├── vpc_gateway_attachment.clj │ │ ├── security_group_egress.clj │ │ ├── route.clj │ │ └── security_group_ingress.clj │ ├── events │ │ ├── network_configuration.clj │ │ ├── aws_vpc_configuration.clj │ │ └── ecs_parameters.clj │ ├── ecs.clj │ ├── cloudformation.clj │ ├── elbv2 │ │ ├── listener_certificate.clj │ │ ├── listener_rule.clj │ │ ├── listener.clj │ │ ├── load_balancer.clj │ │ └── target_group.clj │ ├── auto_scaling.clj │ ├── kinesis.clj │ ├── sqs │ │ ├── redrive_policy.clj │ │ └── queue.clj │ ├── ecr │ │ └── repository.clj │ ├── neptune │ │ ├── db_parameter_group.clj │ │ ├── db_cluster_parameter_group.clj │ │ ├── db_subnet_group.clj │ │ ├── db_instance.clj │ │ └── db_cluster.clj │ ├── custom_resource.clj │ ├── rds │ │ ├── db_subnet_group.clj │ │ └── db_instance.clj │ ├── ssm │ │ └── parameter.clj │ ├── neptune.clj │ ├── elbv2.clj │ ├── sns.clj │ ├── s3 │ │ └── bucket_encryption.clj │ ├── serverless.clj │ ├── auto_scaling │ │ ├── launch_configuration.clj │ │ └── auto_scaling_group.clj │ ├── events.clj │ ├── cloudwatch │ │ └── anomaly_detector.clj │ ├── api_gateway.clj │ ├── route53 │ │ └── record_set.clj │ ├── firehose.clj │ ├── dynamodb.clj │ ├── lambda.clj │ ├── rds.clj │ └── s3.clj │ ├── mappings.clj │ ├── encoding │ ├── keys.clj │ ├── serverless.clj │ └── main.clj │ ├── outputs.clj │ ├── assertion.clj │ ├── parameters.clj │ ├── resources.clj │ ├── encoding.clj │ ├── policies.clj │ ├── core.clj │ └── values.clj ├── deps.edn ├── scripts └── deploy.sh ├── test └── crucible │ ├── xref_test.clj │ ├── aws │ ├── ecr │ │ └── repository_test.clj │ ├── ecs_test.clj │ ├── kms │ │ ├── key_test.clj │ │ └── alias_test.clj │ ├── ec2 │ │ ├── vpc_gateway_attachment_test.clj │ │ ├── subnet_route_table_association_test.clj │ │ ├── route_test.clj │ │ ├── security_group_egress_test.clj │ │ └── security_group_ingress_test.clj │ ├── elbv2 │ │ ├── load_balancer_test.clj │ │ ├── listener_certificate_test.clj │ │ ├── listener_rule_test.clj │ │ ├── listener_test.clj │ │ └── target_group_test.clj │ ├── auto_scaling │ │ ├── launch_configuration_test.clj │ │ └── auto_scaling_group_test.clj │ ├── sqs │ │ └── queue_test.clj │ ├── neptune │ │ ├── db_cluster_test.clj │ │ ├── db_parameter_group_test.clj │ │ ├── db_instance_test.clj │ │ └── db_cluster_parameter_group_test.clj │ ├── serverless │ │ ├── api_test.clj │ │ ├── layer_version_test.clj │ │ ├── simple_table_test.clj │ │ └── function_test.clj │ ├── route53 │ │ └── record_set_test.clj │ ├── ecs │ │ ├── task_test.clj │ │ └── service_test.clj │ ├── cloudwatch │ │ └── anomaly_detector_test.clj │ ├── cloudwatch_test.clj │ ├── events_test.clj │ ├── sns_test.clj │ ├── lambda_test.clj │ ├── firehose_test.clj │ ├── s3_test.clj │ ├── dynamodb_test.clj │ ├── iam_test.clj │ └── ec2_test.clj │ ├── mappings_test.clj │ ├── custom_resource_test.clj │ ├── encoding │ ├── keys_test.clj │ ├── values_test.clj │ └── serverless_test.clj │ ├── resources_test.clj │ ├── values_test.clj │ ├── parameters_test.clj │ └── examples_test.clj ├── .travis.yml ├── test-resources └── aws │ ├── events │ └── simple-event.json │ ├── cloudwatch │ ├── anomaly_detector.json │ └── alarm.json │ ├── s3 │ └── s3-cors.json │ └── dynamodb │ └── complex-table.json ├── CONTRIBUTING.md ├── project.clj └── CODE_OF_CONDUCT.md /demo-project/src/myproject/hello.clj: -------------------------------------------------------------------------------- 1 | (ns myproject.hello) 2 | 3 | (defn -main 4 | [& args] 5 | (println-str "Hello" args)) 6 | -------------------------------------------------------------------------------- /demo-project/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .eastwood 13 | 14 | -------------------------------------------------------------------------------- /src/crucible/aws/ssm.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ssm 2 | "Resources in AWS::SSM::*" 3 | (:require [crucible.resources :refer [defresource]])) 4 | 5 | (defn ssm [suffix] (str "AWS::SSM::" suffix)) 6 | -------------------------------------------------------------------------------- /src/crucible/mappings.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.mappings 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | (s/def ::mapping 5 | (s/map-of string? 6 | (s/map-of 7 | string? 8 | string?))) 9 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {camel-snake-kebab {:mvn/version "0.4.0"} 3 | cheshire {:mvn/version "5.8.0"} 4 | org.clojure/tools.namespace {:mvn/version "0.2.11"} 5 | org.clojure/tools.cli {:mvn/version "0.4.2"} 6 | expound {:mvn/version "0.7.2"}}} 7 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | STABLE_VERSION=${TRAVIS_TAG} 6 | SNAPSHOT_VERSION="${TRAVIS_BRANCH//[\/]/-}-SNAPSHOT" 7 | export PROJECT_VERSION=${STABLE_VERSION:-"${SNAPSHOT_VERSION}"} 8 | echo "Project version is \"$PROJECT_VERSION\"" 9 | 10 | lein deploy 11 | -------------------------------------------------------------------------------- /src/crucible/aws/sqs.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.sqs 2 | "Resources in AWS::SQS::*" 3 | (:require [crucible.resources :refer [defresource]] 4 | [crucible.aws.sqs.queue :as queue])) 5 | 6 | (defn prefix [suffix] (str "AWS::SQS::" suffix)) 7 | 8 | (defresource queue (prefix "Queue") ::queue/resource-spec) 9 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function/event_source/schedule.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function.event-source.schedule 2 | (:require [crucible.resources :refer [spec-or-ref]] 3 | [clojure.spec.alpha :as s])) 4 | 5 | (s/def ::schedule (spec-or-ref string?)) 6 | 7 | (s/def ::properties 8 | (s/keys :req [::schedule])) 9 | -------------------------------------------------------------------------------- /src/crucible/encoding/keys.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.encoding.keys 2 | (:require [camel-snake-kebab.core :refer [->PascalCase]])) 3 | 4 | (defmulti ->key 5 | "Add methods to override default PascalCase encoding where needed, usually for capitalisation" 6 | identity) 7 | 8 | (defmethod ->key :default [kw] 9 | (-> kw name ->PascalCase)) 10 | -------------------------------------------------------------------------------- /src/crucible/aws/ecr.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecr 2 | "Resources in AWS::ECR::*" 3 | (:require [crucible.aws.ecr.repository :as repository] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 5 | 6 | (defn prefix [suffix] (str "AWS::ECR::" suffix)) 7 | 8 | (defresource repository (prefix "Repository") ::repository/resource-spec) 9 | -------------------------------------------------------------------------------- /src/crucible/aws/ecs/cluster.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs.cluster 2 | "Resources in AWS::ECS::Cluster" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref]])) 5 | 6 | (s/def ::cluster-name (spec-or-ref string?)) 7 | 8 | (s/def ::cluster (s/keys :req [] 9 | :opt [::cluster-name])) 10 | -------------------------------------------------------------------------------- /src/crucible/aws/route53.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.route53 2 | "Resources in AWS::Route53::*" 3 | (:require [crucible.aws.route53.record-set :as record-set] 4 | [crucible.resources :as res :refer [defresource]])) 5 | 6 | (defn prefix [suffix] (str "AWS::Route53::" suffix)) 7 | 8 | (defresource record-set (prefix "RecordSet") ::record-set/resource-spec) 9 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function/event_source/sns.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function.event-source.sns 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.resources :refer [spec-or-ref]] 4 | [clojure.spec.alpha :as s])) 5 | 6 | (s/def ::topic ::sam/arn) 7 | 8 | (s/def ::properties 9 | (s/keys :req [::topic])) 10 | -------------------------------------------------------------------------------- /src/crucible/outputs.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.outputs 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.values :as v])) 4 | 5 | (s/def ::name ::v/value) 6 | 7 | (s/def ::export (s/keys :req [::name])) 8 | 9 | (s/def ::description string?) 10 | 11 | (s/def ::value ::v/value) 12 | 13 | (s/def ::output (s/keys :req [::value] 14 | :opt [::description])) 15 | -------------------------------------------------------------------------------- /test/crucible/xref_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.xref-test 2 | (:require [clojure.test :refer :all] 3 | [crucible.values :as v] 4 | [clojure.spec.alpha :as s])) 5 | 6 | (deftest valid-xref 7 | (testing "plain xref validates" 8 | (is (s/valid? ::v/xref (v/xref :foo)))) 9 | 10 | (testing "xref with attribute" 11 | (is (s/valid? ::v/xref (v/xref :foo :bar))))) 12 | -------------------------------------------------------------------------------- /src/crucible/aws/ecs/secret.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs.secret 2 | "AWS::ECS::TaskDefinition > Secret" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref]])) 5 | 6 | (s/def ::name (spec-or-ref string?)) 7 | (s/def ::value-from (spec-or-ref string?)) 8 | 9 | (s/def ::secret-spec (s/keys :req [::name 10 | ::value-from])) 11 | -------------------------------------------------------------------------------- /src/crucible/aws/kms/alias.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.kms.alias 2 | "AWS::KMS::Key" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref] :as res])) 5 | 6 | (s/def ::alias-name (spec-or-ref string?)) 7 | (s/def ::target-key-id (spec-or-ref string?)) 8 | (s/def ::resource-spec (s/keys :req [::alias-name 9 | ::target-key-id])) 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | 3 | dist: trusty 4 | sudo: required 5 | 6 | lein: 2.9.1 7 | 8 | cache: 9 | directories: 10 | - $HOME/.m2 11 | 12 | script: 13 | - if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then lein third-party-check; else lein qa; fi 14 | 15 | deploy: 16 | provider: script 17 | script: scripts/deploy.sh 18 | on: 19 | all_branches: true 20 | condition: $TRAVIS_EVENT_TYPE != cron 21 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function/dead_letter_queue.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function.dead-letter-queue 2 | (:require [crucible.resources :refer [spec-or-ref]] 3 | [clojure.spec.alpha :as s])) 4 | 5 | (s/def ::type #{"SNS" "SQS"}) 6 | 7 | (s/def ::target-arn (spec-or-ref string?)) 8 | 9 | (s/def ::dead-letter-queue (s/keys :req [::type 10 | ::target-arn])) 11 | -------------------------------------------------------------------------------- /src/crucible/aws/ecs/key_value_pair.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs.key-value-pair 2 | "AWS::ECS::TaskDefinition > ContainerDefinition > KeyValuePair" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref]])) 5 | 6 | (s/def ::name (spec-or-ref string?)) 7 | (s/def ::value (spec-or-ref string?)) 8 | 9 | (s/def ::entity-spec (s/keys :req [::name 10 | ::value])) 11 | -------------------------------------------------------------------------------- /test/crucible/aws/ecr/repository_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecr.repository-test 2 | (:require [crucible.aws.ecr.repository :as repository] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest repository-tests 7 | 8 | (testing "valid repository" 9 | (is 10 | (s/valid? ::repository/resource-spec 11 | {::repository/repository-name "infrastructure/deployment"})))) 12 | -------------------------------------------------------------------------------- /src/crucible/aws/kms.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.kms 2 | "Resources in AWS::KMS::*" 3 | (:require [crucible.resources :refer [spec-or-ref defresource] :as res] 4 | [crucible.aws.kms.key :as key] 5 | [crucible.aws.kms.alias :as alias])) 6 | 7 | (defn prefix [suffix] (str "AWS::KMS::" suffix)) 8 | 9 | (defresource key (prefix "Key") ::key/resource-spec) 10 | 11 | (defresource alias (prefix "Alias") ::alias/resource-spec) 12 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function/event_source/sqs.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function.event-source.sqs 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.resources :refer [spec-or-ref]] 4 | [clojure.spec.alpha :as s])) 5 | 6 | (s/def ::queue ::sam/arn) 7 | 8 | (s/def ::batch-size (spec-or-ref integer?)) 9 | 10 | (s/def ::properties 11 | (s/keys :req [::queue] 12 | :opt [::batch-size])) 13 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function/event_source/api.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function.event-source.api 2 | (:require [crucible.resources :refer [spec-or-ref]] 3 | [clojure.spec.alpha :as s])) 4 | 5 | (s/def ::path (spec-or-ref string?)) 6 | 7 | (s/def ::method (spec-or-ref string?)) 8 | 9 | (s/def ::rest-api-id (spec-or-ref string?)) 10 | 11 | (s/def ::properties 12 | (s/keys :req [::path 13 | ::method])) 14 | -------------------------------------------------------------------------------- /test/crucible/aws/ecs_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs-test 2 | (:require [clojure.test :refer :all] 3 | [crucible.aws.ecs :as ecs] 4 | [crucible.resources :as res] 5 | [clojure.spec.alpha :as s] 6 | [crucible.core :refer [template encode parameter xref]])) 7 | 8 | (deftest ecs-cluster-test 9 | (testing "minimal spec" 10 | (is (s/valid? ::res/resource (second (ecs/cluster {::ecs/cluster-name "cluster-one"})))))) 11 | -------------------------------------------------------------------------------- /src/crucible/aws/ec2/route_table.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.route-table 2 | "AWS::EC2::RouteTable" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.ec2 :as ec2] 5 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 6 | 7 | (s/def ::vpc-id ::ec2/vpc-id) 8 | 9 | (s/def ::route-table (s/keys :req [::vpc-id] 10 | :opt [::res/tags])) 11 | 12 | (defresource route-table (ec2/ec2 "RouteTable") ::route-table) 13 | -------------------------------------------------------------------------------- /test/crucible/mappings_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.mappings-test 2 | (:require [crucible.core :refer [mapping]] 3 | [crucible.encoding :as enc] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest mapping-rewrite-test 7 | (testing "Rewrite is a no-op" 8 | (is (= {"foo" {"bar" "baz" 9 | "quxx" "fizz"}} 10 | (enc/rewrite-element-data (mapping "foo" {"bar" "baz" 11 | "quxx" "fizz"})))))) 12 | -------------------------------------------------------------------------------- /src/crucible/aws/events/network_configuration.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.events.network-configuration 2 | "AWS::ECS::Rule > NetworkConfiguration" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.events.aws-vpc-configuration :as aws-vpc-configuration] 5 | [crucible.resources :refer [spec-or-ref]])) 6 | 7 | (s/def ::aws-vpc-configuration ::aws-vpc-configuration/aws-vpc-configuration-spec) 8 | 9 | (s/def ::network-configuration-spec (s/keys :opt [::aws-vpc-configuration])) 10 | -------------------------------------------------------------------------------- /test/crucible/aws/kms/key_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.kms.key-test 2 | (:require [crucible.aws.kms.key :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest key-tests 8 | (testing "valid key" 9 | (is (s/valid? ::sut/resource-spec 10 | {::sut/description (join "-" [cf/stack-name "master-key"]) 11 | ::sut/key-policy {:version "2012-10-17"}})))) 12 | -------------------------------------------------------------------------------- /test/crucible/aws/kms/alias_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.kms.alias-test 2 | (:require [crucible.aws.kms.alias :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest alias-tests 8 | (testing "valid alias" 9 | (is (s/valid? ::sut/resource-spec 10 | {::sut/alias-name (join "-" [cf/stack-name "master-key-alias"]) 11 | ::sut/target-key-id (xref :env-key)})))) 12 | -------------------------------------------------------------------------------- /test-resources/aws/events/simple-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::Events::Rule", 3 | "Properties": { 4 | "Description": "EventRule", 5 | "EventPattern": { 6 | "detail-type": [ "AWS API Call via CloudTrail" ], 7 | "detail": { 8 | "userIdentity": { 9 | "type": [ "Root" ] 10 | } 11 | } 12 | }, 13 | "State": "ENABLED", 14 | "Targets": [ 15 | { 16 | "Arn": { "Ref": "MySnsTopic" }, 17 | "Id": "OpsTopic" 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/crucible/aws/ec2/vpc_gateway_attachment_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.vpc-gateway-attachment-test 2 | (:require [crucible.aws.ec2.vpc-gateway-attachment :as sut] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest vpc-gateway-attachment-tests 7 | 8 | (testing "valid vpc gateway attachment definition" 9 | (is (s/valid? ::sut/vpc-gateway-attachment {::sut/vpc-id "some-vpc-id" 10 | ::sut/internet-gateway-id "some-internet-gateway-id"})))) 11 | -------------------------------------------------------------------------------- /src/crucible/aws/ecs.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs 2 | "Resources in AWS::ECS::*" 3 | (:require [crucible.resources :refer [defresource]] 4 | [crucible.aws.ecs.cluster :as cl] 5 | [crucible.aws.ecs.task-definition :as td] 6 | [crucible.aws.ecs.service :as svc])) 7 | 8 | (defn ecs [suffix] (str "AWS::ECS::" suffix)) 9 | 10 | (defresource cluster (ecs "Cluster") ::cl/cluster) 11 | 12 | (defresource service (ecs "Service") ::svc/service) 13 | 14 | (defresource task-definition (ecs "TaskDefinition") ::td/task-definition) 15 | -------------------------------------------------------------------------------- /src/crucible/aws/cloudformation.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.cloudformation 2 | "Resources in AWS::CloudFormation::*" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res] 5 | [crucible.encoding.keys :refer [->key]])) 6 | 7 | (defmethod ->key :template-url [_] "TemplateURL") 8 | 9 | (s/def ::parameters (s/keys)) 10 | 11 | (s/def ::template-url (spec-or-ref string?)) 12 | 13 | (s/def ::stack (s/keys ::req [::template-url ::parameters])) 14 | 15 | (defresource stack "AWS::CloudFormation::Stack" ::stack) 16 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function/event_source/kinesis.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function.event-source.kinesis 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.resources :refer [spec-or-ref]] 4 | [clojure.spec.alpha :as s])) 5 | 6 | (s/def ::stream ::sam/arn) 7 | 8 | (s/def ::starting-position (spec-or-ref #{"TRIM_HORIZON" "LATEST"})) 9 | 10 | (s/def ::batch-size (spec-or-ref integer?)) 11 | 12 | (s/def ::properties 13 | (s/keys :req [::stream 14 | ::starting-position] 15 | :opt [::batch-size])) 16 | -------------------------------------------------------------------------------- /test-resources/aws/cloudwatch/anomaly_detector.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type" : "AWS::CloudWatch::AnomalyDetector", 3 | "Properties" : { 4 | "Configuration" : { 5 | "ExcludedTimeRanges": [{ 6 | "EndTime": "2019-07-01T23:59:59", 7 | "StartTime": "2019-07-01T23:59:59" 8 | }], 9 | "MetricTimeZone": "America/New_York" 10 | }, 11 | "Dimensions" : [{ 12 | "Name": "Memory", 13 | "Value": "UsedMemory" 14 | }], 15 | "MetricName" : "JvmMetric", 16 | "Namespace" : "AWSSDK/Java", 17 | "Stat" : "Average" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/crucible/aws/elbv2/listener_certificate.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.listener-certificate 2 | "AWS::ElasticLoadBalancingV2::Listener" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 5 | 6 | (s/def ::certificate-arn (spec-or-ref string?)) 7 | (s/def ::certificate (s/keys :req [::certificate-arn])) 8 | (s/def ::certificates (s/* ::certificate)) 9 | (s/def ::listener-arn (spec-or-ref string?)) 10 | 11 | (s/def ::resource-spec (s/keys :req [::certificates 12 | ::listener-arn])) 13 | -------------------------------------------------------------------------------- /test/crucible/aws/ec2/subnet_route_table_association_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.subnet-route-table-association-test 2 | (:require [crucible.aws.ec2.subnet-route-table-association :as sut] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest subnet-route-table-association-tests 7 | 8 | (testing "valid subnet route table association definition" 9 | (is (s/valid? ::sut/subnet-route-table-association {::sut/subnet-id "some-subnet-id" 10 | ::sut/route-table-id "some-route-table-id"})))) 11 | -------------------------------------------------------------------------------- /test/crucible/aws/ec2/route_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.route-test 2 | (:require [crucible.aws.ec2.route :as sut] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest route-tests 7 | 8 | (testing "valid gateway id" (is (s/valid? ::sut/gateway-id "sample-gateway-id"))) 9 | 10 | (testing "nat route" (is (s/valid? ::sut/route {::sut/route-table-id "some-routetable" 11 | ::sut/nat-gateway-id "some-gateway" 12 | ::sut/destination-cidr-block "0.0.0.0/0"})))) 13 | -------------------------------------------------------------------------------- /test/crucible/aws/elbv2/load_balancer_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.load-balancer-test 2 | (:require [crucible.aws.elbv2.load-balancer :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest load-balancer-tests 8 | 9 | (testing "valid load balancer" 10 | (is (s/valid? ::sut/resource-spec 11 | {::sut/name (join "-" [cf/stack-name "crucible"]) 12 | ::sut/subnets [(xref :public1) (xref :public2)] 13 | ::sut/security-groups [(xref :sg-public)]})))) 14 | -------------------------------------------------------------------------------- /src/crucible/aws/auto_scaling.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.auto-scaling 2 | "Resources in AWS::AutoScaling::*" 3 | (:require [crucible.aws.auto-scaling.auto-scaling-group :as auto-scaling-group] 4 | [crucible.aws.auto-scaling.launch-configuration :as launch-configuration] 5 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 6 | 7 | (defn prefix [suffix] (str "AWS::AutoScaling::" suffix)) 8 | 9 | (defresource auto-scaling-group (prefix "AutoScalingGroup") ::auto-scaling-group/resource-spec) 10 | (defresource launch-configuration (prefix "LaunchConfiguration") ::launch-configuration/resource-spec) 11 | -------------------------------------------------------------------------------- /src/crucible/aws/events/aws_vpc_configuration.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.events.aws-vpc-configuration 2 | "AWS::ECS::Rule > AwsVpcConfiguration" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref]])) 5 | 6 | (s/def ::assign-public-ip (spec-or-ref string?)) 7 | 8 | (s/def ::security-groups (s/coll-of string? :type vector)) 9 | 10 | (s/def ::subnets (s/coll-of string? :type vector)) 11 | 12 | (s/def ::aws-vpc-configuration-spec (s/keys :req [::subnets] 13 | :opt [::assign-public-ip 14 | ::security-groups])) 15 | -------------------------------------------------------------------------------- /src/crucible/aws/kinesis.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.kinesis 2 | "Resources in AWS::Kinesis::*" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource]])) 5 | 6 | (s/def ::shard-count (spec-or-ref pos-int?)) 7 | (s/def ::retention-period-hours (spec-or-ref pos-int?)) 8 | 9 | (s/def ::name (spec-or-ref string?)) 10 | 11 | (s/def ::stream (s/keys :req [::shard-count] 12 | :opt [::name 13 | ::retention-period-hours 14 | :crucible.resources/tags])) 15 | 16 | (defresource stream "AWS::Kinesis::Stream" ::stream) 17 | -------------------------------------------------------------------------------- /src/crucible/aws/kms/key.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.kms.key 2 | "AWS::KMS::Key" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref] :as res])) 5 | 6 | (s/def ::description (spec-or-ref string?)) 7 | (s/def ::enabled (spec-or-ref boolean?)) 8 | (s/def ::enable-key-rotation (spec-or-ref boolean?)) 9 | (s/def ::key-policy (spec-or-ref any?)) 10 | 11 | (s/def ::resource-spec (s/keys :req [::key-policy] 12 | :opt [::description 13 | ::enabled 14 | ::enable-key-rotation 15 | ::res/tags])) 16 | -------------------------------------------------------------------------------- /test/crucible/aws/elbv2/listener_certificate_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.listener-certificate-test 2 | (:require [crucible.aws.elbv2.listener-certificate :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest listener-certificate-tests 8 | 9 | (testing "valid listener certificate" 10 | (is (s/valid? ::sut/resource-spec 11 | {::sut/listener-arn (xref :listener-io) 12 | ::sut/certificates [{::sut/certificate-arn 13 | (cf/find-in-map :environments cf/stack-name "CertificateArn")}]})))) 14 | -------------------------------------------------------------------------------- /demo-project/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sample-s3-bucket "0.1.0-SNAPSHOT" 2 | :description "Demonstration of Crucible" 3 | :url "http://github.com/brabster/crucible" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.9.0-alpha10"]] 7 | :target-path "target/%s" 8 | 9 | ;; alias "lein templates" to find and encode any templates in the project 10 | :aliases {"templates" ["run" "-m" crucible.encoding.main]} 11 | 12 | ;; put your templates in the "templates" directory 13 | :profiles {:dev {:source-paths ["templates"] 14 | :dependencies [[crucible "0.10.0-SNAPSHOT"]]}}) 15 | -------------------------------------------------------------------------------- /src/crucible/aws/ec2/subnet_route_table_association.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.subnet-route-table-association 2 | "AWS::EC2::SubnetRouteTableAssociation" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.ec2 :as ec2] 5 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 6 | 7 | (s/def ::route-table-id (spec-or-ref string?)) 8 | 9 | (s/def ::subnet-id (spec-or-ref string?)) 10 | 11 | (s/def ::subnet-route-table-association (s/keys :req [::route-table-id 12 | ::subnet-id])) 13 | 14 | (defresource subnet-route-table-association (ec2/ec2 "SubnetRouteTableAssociation") ::subnet-route-table-association) 15 | -------------------------------------------------------------------------------- /src/crucible/aws/sqs/redrive_policy.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.sqs.redrive-policy 2 | "Amazon SQS RedrivePolicy, a property of AWS::SQS::Queue" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.encoding.keys :refer [->key]] 5 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 6 | 7 | (s/def ::dead-letter-target-arn (spec-or-ref string?)) 8 | (s/def ::max-receive-count (spec-or-ref integer?)) 9 | 10 | (defmethod ->key :max-receive-count [_] "maxReceiveCount") 11 | (defmethod ->key :dead-letter-target-arn [_] "deadLetterTargetArn") 12 | 13 | (s/def ::resource-property-spec (s/keys :req [::dead-letter-target-arn 14 | ::max-receive-count])) 15 | -------------------------------------------------------------------------------- /src/crucible/aws/ec2/vpc_gateway_attachment.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.vpc-gateway-attachment 2 | "AWS::EC2::VPCGatewayAttachment" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource]] 5 | [crucible.aws.ec2 :as ec2])) 6 | 7 | (s/def ::vpc-id ::ec2/vpc-id) 8 | (s/def ::internet-gateway-id (spec-or-ref string?)) 9 | (s/def ::vpn-gateway-id (spec-or-ref string?)) 10 | 11 | (s/def ::vpc-gateway-attachment (s/keys :req [::vpc-id] 12 | :opt [::internet-gateway-id 13 | ::vpn-gateway-id])) 14 | 15 | (defresource vpc-gateway-attachment (ec2/ec2 "VPCGatewayAttachment") ::vpc-gateway-attachment) 16 | -------------------------------------------------------------------------------- /src/crucible/aws/ecr/repository.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecr.repository 2 | "AWS::ECR::Repository" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref] :as res])) 5 | 6 | (s/def ::lifecycle-policy-text (spec-or-ref string?)) 7 | (s/def ::registry-id (spec-or-ref string?)) 8 | 9 | (s/def ::lifecycle-policy (s/keys :opt [::lifecycle-policy-text 10 | ::registry-id])) 11 | (s/def ::repository-name (spec-or-ref string?)) 12 | (s/def ::repository-policy-text (spec-or-ref map?)) 13 | 14 | (s/def ::resource-spec (s/keys :opt [::lifecycle-policy 15 | ::repository-name 16 | ::repository-policy-text])) 17 | -------------------------------------------------------------------------------- /src/crucible/aws/neptune/db_parameter_group.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-parameter-group 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.aws.neptune :as neptune] 4 | [crucible.resources :refer [spec-or-ref defresource]])) 5 | 6 | (s/def ::description ::neptune/description) 7 | 8 | (s/def ::family ::neptune/family) 9 | 10 | (s/def ::parameters ::neptune/parameters) 11 | 12 | (s/def ::name ::neptune/name) 13 | 14 | (s/def ::db-parameter-group 15 | (s/keys :req [::description 16 | ::family] 17 | :opt [::parameters 18 | :crucible.resources/tags 19 | ::name])) 20 | 21 | (defresource db-parameter-group "AWS::Neptune::DBParameterGroup" ::db-parameter-group) 22 | -------------------------------------------------------------------------------- /test/crucible/aws/auto_scaling/launch_configuration_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.auto-scaling.launch-configuration-test 2 | (:require [crucible.aws.auto-scaling.launch-configuration :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest launch-configuration-tests 8 | (testing "valid launch configuration" 9 | (is (s/valid? ::sut/resource-spec 10 | {::sut/key-name "secret" 11 | ::sut/image-id "ami-something" 12 | ::sut/instance-type (xref :instance-type)}))) 13 | (testing "invalid launch configuration" 14 | (is (not (s/valid? ::sut/resource-spec 15 | {}))))) 16 | -------------------------------------------------------------------------------- /test/crucible/aws/ec2/security_group_egress_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.security-group-egress-test 2 | (:require [crucible.aws.ec2.security-group-egress :as sut] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest security-group-egress-tests 7 | 8 | (testing "valid security group egress rule" 9 | (is (s/valid? ::sut/security-group-egress {::sut/ip-protocol "tcp" 10 | ::sut/from-port 3000 11 | ::sut/to-port 3000 12 | ::sut/group-id "some-group-id" 13 | ::sut/destination-security-group-id "some-security-group-id"})))) 14 | -------------------------------------------------------------------------------- /test/crucible/aws/ec2/security_group_ingress_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.security-group-ingress-test 2 | (:require [crucible.aws.ec2.security-group-ingress :as sut] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest security-group-ingress-tests 7 | 8 | (testing "valid security group ingress rule" 9 | (is (s/valid? ::sut/security-group-ingress {::sut/ip-protocol "tcp" 10 | ::sut/from-port 4334 11 | ::sut/to-port 4336 12 | ::sut/source-security-group-id "some-security-group-id" 13 | ::sut/group-id "some-group-id"})))) 14 | -------------------------------------------------------------------------------- /test/crucible/aws/elbv2/listener_rule_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.listener-rule-test 2 | (:require [crucible.aws.elbv2.listener-rule :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest listener-rule-tests 8 | 9 | (testing "valid listener rule" 10 | (is (s/valid? ::sut/resource-spec 11 | {::sut/actions [{::sut/target-group-arn (xref :target-group) 12 | ::sut/type "forward"}] 13 | ::sut/conditions [{::sut/field "host-header" 14 | ::sut/values ["app.crucible.io"]}] 15 | ::sut/priority 1 16 | ::sut/listener-arn (xref :listener-io)})))) 17 | -------------------------------------------------------------------------------- /src/crucible/assertion.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.assertion 2 | (:require [clojure.test :as test] 3 | [crucible.encoding :as enc])) 4 | 5 | (def resource= 'encoded-as) 6 | 7 | ;; clojure.test doesn't print ex-data which is a pain with clojure.spec.alpha 8 | ;; remove when ex-data is printed on test failures by default... 9 | (defmethod test/assert-expr 'encoded-as [msg form] 10 | (let [expected (nth form 1) 11 | resource (nth form 2)] 12 | `(let [result# (try (enc/rewrite-element-data ~resource) 13 | (catch ExceptionInfo e# (ex-data e#)))] 14 | (if (= ~expected result#) 15 | (test/do-report {:type :pass :expected ~expected :actual ~resource :message ~msg}) 16 | (test/do-report {:type :fail :expected ~expected :actual result# :message ~msg}))))) 17 | -------------------------------------------------------------------------------- /src/crucible/aws/neptune/db_cluster_parameter_group.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-cluster-parameter-group 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.aws.neptune :as neptune] 4 | [crucible.resources :refer [spec-or-ref defresource]])) 5 | 6 | (s/def ::description ::neptune/description) 7 | 8 | (s/def ::family ::neptune/family) 9 | 10 | (s/def ::parameters ::neptune/parameters) 11 | 12 | (s/def ::name ::neptune/name) 13 | 14 | (s/def ::db-cluster-parameter-group 15 | (s/keys :req [::description 16 | ::parameters 17 | ::family] 18 | :opt [:crucible.resources/tags 19 | ::name])) 20 | 21 | (defresource db-cluster-parameter-group 22 | "AWS::Neptune::DBClusterParameterGroup" 23 | ::db-cluster-parameter-group) 24 | -------------------------------------------------------------------------------- /test/crucible/aws/sqs/queue_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.sqs.queue-test 2 | (:require [crucible.aws.sqs.queue :as sut] 3 | [crucible.aws.sqs.redrive-policy :as redrive-policy] 4 | [crucible.core :refer [xref join] :as cf] 5 | [clojure.spec.alpha :as s] 6 | [clojure.test :refer :all])) 7 | 8 | (deftest listener-tests 9 | 10 | (testing "valid minimal queue" 11 | (is (s/valid? ::sut/resource-spec 12 | {::sut/queue-name "webhooks"}))) 13 | (testing "queue with redrive policy" 14 | (is (s/valid? ::sut/resource-spec 15 | {::sut/queue-name "user-event" 16 | ::sut/redrive-policy 17 | {::redrive-policy/dead-letter-target-arn (xref :user-event-dl) 18 | ::redrive-policy/max-receive-count 3}})))) 19 | -------------------------------------------------------------------------------- /test/crucible/aws/neptune/db_cluster_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-cluster-test 2 | (:require [crucible.aws.neptune.db-cluster :as dbc] 3 | [crucible.resources :as r] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest neptune-db-cluster-test 7 | (testing "encode" 8 | (is (= {"Type" "AWS::Neptune::DBCluster", 9 | "Properties" 10 | {"DBClusterIdentifier" "abc-def-123", 11 | "IamAuthEnabled" true, 12 | "Tags" [{"Key" "key", "Value" "value"}]}} 13 | (crucible.encoding/rewrite-element-data 14 | (dbc/db-cluster {::dbc/db-cluster-identifier "abc-def-123" 15 | ::dbc/iam-auth-enabled true 16 | ::r/tags [{::r/key "key" 17 | ::r/value "value"}]})))))) 18 | -------------------------------------------------------------------------------- /test/crucible/aws/neptune/db_parameter_group_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-parameter-group-test 2 | (:require [crucible.aws.neptune.db-parameter-group :as dbpg] 3 | [crucible.resources :as r] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest neptune-db-parameter-group-test 7 | (testing "encode" 8 | (is (= {"Type" "AWS::Neptune::DBParameterGroup", 9 | "Properties" 10 | {"Description" "description", 11 | "Family" "neptune1", 12 | "Tags" [{"Key" "key", "Value" "value"}]}} 13 | (crucible.encoding/rewrite-element-data 14 | (dbpg/db-parameter-group 15 | {::dbpg/description "description" 16 | ::dbpg/family "neptune1" 17 | ::r/tags [{::r/key "key" 18 | ::r/value "value"}]})))))) 19 | -------------------------------------------------------------------------------- /test-resources/aws/cloudwatch/alarm.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::CloudWatch::Alarm", 3 | "Properties": { 4 | "AlarmDescription": "Scale-up if CPU is greater than 90% for 10 minutes", 5 | "MetricName": "CPUUtilization", 6 | "Namespace": "AWS/EC2", 7 | "Statistic": "Average", 8 | "Period": "300", 9 | "EvaluationPeriods": "2", 10 | "Threshold": "90", 11 | "AlarmActions": [ 12 | { 13 | "Ref": "WebServerScaleUpPolicy" 14 | } 15 | ], 16 | "Dimensions": [ 17 | { 18 | "Name": "AutoScalingGroupName", 19 | "Value": { 20 | "Ref": "WebServerGroup" 21 | } 22 | } 23 | ], 24 | "ComparisonOperator": "GreaterThanThreshold" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/crucible/aws/serverless/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.api-test 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.aws.serverless.api :as api] 4 | [crucible.core :refer [xref]] 5 | [crucible.resources :as res] 6 | [clojure.test :refer :all] 7 | [crucible.encoding :as encoding])) 8 | 9 | (deftest api-test 10 | (testing "encode" 11 | (is (= {"Type" "AWS::Serverless::Api" 12 | "Properties" {"Name" "my-api" 13 | "StageName" "LATEST" 14 | "Cors" {"AllowOrigin" "www.example.com"}}} 15 | (encoding/rewrite-element-data 16 | (api/api 17 | {::api/name "my-api" 18 | ::api/stage-name "LATEST" 19 | ::api/cors {::sam/allow-origin "www.example.com"}})))))) 20 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/simple_table.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.simple-table 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.aws.dynamodb :as ddb] 4 | [crucible.resources :refer [spec-or-ref defresource]] 5 | [clojure.spec.alpha :as s])) 6 | 7 | (s/def ::primary-key (spec-or-ref ::sam/primary-key)) 8 | 9 | (s/def ::provisioned-throughput ::ddb/provisioned-throughput) 10 | 11 | (s/def ::tags ::sam/tags) 12 | 13 | (s/def ::table-name (spec-or-ref string?)) 14 | 15 | (s/def ::sse-specification ::ddb/sse-specification) 16 | 17 | (s/def ::simple-table 18 | (s/keys :opt [::primary-key 19 | ::provisioned-throughput 20 | ::tags 21 | ::table-name 22 | ::sse-specification])) 23 | 24 | (defresource simple-table "AWS::Serverless::SimpleTable" ::simple-table) 25 | -------------------------------------------------------------------------------- /test/crucible/aws/elbv2/listener_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.listener-test 2 | (:require [crucible.aws.elbv2.listener :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest listener-tests 8 | 9 | (testing "valid listener" 10 | (is (s/valid? ::sut/resource-spec 11 | {::sut/default-actions [{::sut/target-group-arn (xref :target-group) 12 | ::sut/type "forward"}] 13 | ::sut/load-balancer-arn (xref :load-balancer) 14 | ::sut/port 443 15 | ::sut/protocol "HTTPS" 16 | ::sut/certificates [{::sut/certificate-arn 17 | (cf/find-in-map :environments cf/stack-name "CertificateArn")}]})))) 18 | -------------------------------------------------------------------------------- /src/crucible/aws/custom_resource.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.custom-resource 2 | "AWS Custom Resources have a type that is defined by the client, 3 | prefixed with 'Custom::'. Clients must therefore specify this name 4 | when using the resource." 5 | (:require [crucible.resources :refer [spec-or-ref resource-factory]] 6 | [crucible.encoding.keys :refer [->key]] 7 | [clojure.spec.alpha :as s])) 8 | 9 | (s/def ::service-token (spec-or-ref string?)) 10 | 11 | (defn resource 12 | "Define an AWS Custom::ResourceName named for the resource-name 13 | argument. Optionally pass parameters directly, for example where the 14 | definition will not be reused." 15 | ([resource-name parameters] 16 | ((resource resource-name) parameters)) 17 | ([resource-name] 18 | (resource-factory (str "Custom::" (->key resource-name)) (s/keys ::req [::service-token])))) 19 | -------------------------------------------------------------------------------- /test/crucible/custom_resource_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.custom-resource-test 2 | (:require [clojure.test :refer :all] 3 | [crucible.assertion :refer [resource=]] 4 | [crucible.aws.custom-resource :as custom] 5 | [cheshire.core :as json])) 6 | 7 | (deftest custom-resource-apitest 8 | (let [service-token "arn:foo" 9 | expected {"Type" "Custom::MyResource" 10 | "Properties" {"ServiceToken" service-token}}] 11 | 12 | (testing "higher-order resource fn" 13 | (is (resource= expected 14 | ((custom/resource "MyResource") 15 | {::custom/service-token service-token})))) 16 | 17 | (testing "higher-order resource fn" 18 | (is (resource= expected 19 | (custom/resource "MyResource" 20 | {::custom/service-token service-token})))))) 21 | -------------------------------------------------------------------------------- /src/crucible/aws/rds/db_subnet_group.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.rds.db-subnet-group 2 | "AWS::RDS::DBSubnetGroup" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref] :as res] 5 | [crucible.encoding.keys :refer [->key]])) 6 | 7 | (defmethod ->key :db-subnet-group-name [_] "DBSubnetGroupName") 8 | (defmethod ->key :db-subnet-group-description [_] "DBSubnetGroupDescription") 9 | 10 | (s/def ::db-subnet-group-name (spec-or-ref string?)) 11 | 12 | (s/def ::db-subnet-group-description (spec-or-ref string?)) 13 | 14 | (s/def ::subnet-ids (s/coll-of (spec-or-ref string?) :kind vector?)) 15 | 16 | (s/def ::db-subnet-group-spec (s/keys :req [::db-subnet-group-description 17 | ::subnet-ids] 18 | :opt [::db-subnet-group-name 19 | ::res/tags])) 20 | -------------------------------------------------------------------------------- /test/crucible/aws/elbv2/target_group_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.target-group-test 2 | (:require [crucible.aws.elbv2.target-group :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest target-group-tests 8 | 9 | (testing "valid target group" 10 | (is (s/valid? ::sut/resource-spec 11 | {::sut/vpc-id (xref :vpc) 12 | ::sut/port 80 13 | ::sut/protocol "HTTP" 14 | ::sut/target-type "ip" 15 | ::sut/health-check-path "/ping" 16 | ::sut/health-check-port "80" 17 | ::sut/health-check-protocol "HTTP" 18 | ::sut/target-group-attributes [{::sut/key "stickiness.enabled" 19 | ::sut/value "true"}]})))) 20 | -------------------------------------------------------------------------------- /src/crucible/aws/neptune/db_subnet_group.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-subnet-group 2 | "Specs for AWS::Neptune::DBSubnetGroup" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.neptune :as neptune] 5 | [crucible.encoding.keys :refer [->key]] 6 | [crucible.resources :refer [spec-or-ref defresource]])) 7 | 8 | (s/def ::db-subnet-group-description (spec-or-ref string?)) 9 | (defmethod ->key :db-subnet-group-description [_] "DBSubnetGroupDescription") 10 | 11 | (s/def ::db-subnet-group-name ::neptune/db-subnet-group-name) 12 | 13 | (s/def ::subnet-ids (spec-or-ref (s/* string?))) 14 | 15 | (s/def ::db-subnet-group 16 | (s/keys :req [::db-subnet-group-description 17 | ::subnet-ids] 18 | :opt [::db-subnet-group-name 19 | :crucible.resources/tags])) 20 | 21 | (defresource db-subnet-group "AWS::Neptune::DBSubnetGroup" ::db-subnet-group) 22 | -------------------------------------------------------------------------------- /src/crucible/aws/ssm/parameter.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ssm.parameter 2 | "AWS::SSM::Parameter" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.ssm :refer [ssm]] 5 | [crucible.encoding.keys :refer [->key]] 6 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 7 | 8 | (s/def ::type (spec-or-ref string?)) 9 | (s/def ::value (spec-or-ref string?)) 10 | (s/def ::allowed-pattern (spec-or-ref string?)) 11 | (s/def ::description (spec-or-ref string?)) 12 | (s/def ::name (spec-or-ref string?)) 13 | (s/def ::tier (spec-or-ref string?)) 14 | 15 | (s/def ::parameter-spec 16 | (s/keys :req [::type 17 | ::value] 18 | :opt [::allowed-pattern 19 | ::description 20 | ::name 21 | ::policies 22 | ::res/tags 23 | ::tier])) 24 | 25 | (defresource parameter (ssm "Parameter") ::parameter-spec) 26 | -------------------------------------------------------------------------------- /test/crucible/aws/route53/record_set_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.route53.record-set-test 2 | (:require [crucible.aws.route53.record-set :as sut] 3 | [crucible.core :refer [xref join] :as cf] 4 | [clojure.spec.alpha :as s] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest route-tests 8 | 9 | (testing "valid record set" 10 | (is (s/valid? ::sut/resource-spec 11 | {::sut/name "www.crucible.io" 12 | ::sut/type "A" 13 | ::sut/hosted-zone-name "atlascrm.io." 14 | ::sut/alias-target {::sut/hosted-zone-id (xref :load-balancer :canonical-hosted-zone-name-i-d) 15 | ::sut/evaluate-target-health "false" 16 | ::sut/resource-records ["http://example.com"] 17 | ::sut/dns-name (xref :load-balancer :canonical-hosted-zone-name)}})))) 18 | -------------------------------------------------------------------------------- /src/crucible/aws/neptune.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune 2 | "Resources in AWS::Neptune::*" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.encoding.keys :refer [->key]] 5 | [crucible.resources :refer [spec-or-ref defresource]])) 6 | 7 | ;;; property specs shared among AWS::Neptune::* resources 8 | 9 | (def db-cluster-identifier-regex #"^[a-zA-Z][a-zA-Z0-9_\-]*[a-zA-Z0-9_]") 10 | (s/def ::db-cluster-identifier 11 | (spec-or-ref (s/and string? #(re-matches db-cluster-identifier-regex %)))) 12 | (defmethod ->key :db-cluster-identifier [_] "DBClusterIdentifier") 13 | 14 | (s/def ::db-subnet-group-name (spec-or-ref string?)) 15 | (defmethod ->key :db-subnet-group-name [_] "DBSubnetGroupName") 16 | 17 | (s/def ::description (spec-or-ref string?)) 18 | 19 | (s/def ::family (spec-or-ref #{"neptune1"})) 20 | 21 | (s/def ::name (spec-or-ref string?)) 22 | 23 | (s/def ::parameters (spec-or-ref (s/map-of string? string?))) 24 | -------------------------------------------------------------------------------- /src/crucible/aws/ec2/security_group_egress.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.security-group-egress 2 | "AWS::EC2::SecurityGroupEgress" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource]] 5 | [crucible.aws.ec2 :as ec2])) 6 | 7 | (s/def ::security-group-egress (s/keys :req [::ip-protocol] 8 | :opt [::cidr-ip 9 | ::cidr-ipv6 10 | ::description 11 | ::from-port 12 | ::to-port 13 | ::group-id 14 | ::destination-prefix-list-id 15 | ::destination-security-group-id])) 16 | 17 | (defresource security-group-egress (ec2/ec2 "SecurityGroupEgress") ::security-group-egress) 18 | -------------------------------------------------------------------------------- /test/crucible/aws/auto_scaling/auto_scaling_group_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.auto-scaling.auto-scaling-group-test 2 | (:require [crucible.aws.auto-scaling.auto-scaling-group :as sut] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest auto-scaling-group-tests 7 | (testing "valid auto scaling group" 8 | (is (s/valid? ::sut/resource-spec 9 | {::sut/min-size "2" 10 | ::sut/max-size "2" 11 | ::sut/availability-zones ["us-east-1"] 12 | ::sut/tags [{::sut/key "Name" 13 | ::sut/value "Testing" 14 | ::sut/propagate-at-launch true 15 | ::sut/resource-type "auto-scaling-group" 16 | ::sut/resource-id "my-asg"}]}))) 17 | (testing "invalid auto scaling group" 18 | (is (not (s/valid? ::sut/resource-spec 19 | {}))))) 20 | -------------------------------------------------------------------------------- /test/crucible/aws/neptune/db_instance_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-instance-test 2 | (:require [crucible.aws.neptune.db-instance :as dbi] 3 | [crucible.resources :as r] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest neptune-instance-test 7 | (testing "encode" 8 | (is (= {"Type" "AWS::Neptune::DBInstance", 9 | "Properties" 10 | {"AllowMajorVersionUpgrade" false, 11 | "AutoMinorVersionUpgrade" true, 12 | "DBInstanceClass" "db.r4.large", 13 | "Tags" [{"Key" "key", "Value" "value"}]}} 14 | (crucible.encoding/rewrite-element-data 15 | (dbi/db-instance {::dbi/allow-major-version-upgrade false 16 | ::dbi/auto-minor-version-upgrade true 17 | ::dbi/db-instance-class "db.r4.large" 18 | ::r/tags [{::r/key "key" 19 | ::r/value "value"}]})))))) 20 | -------------------------------------------------------------------------------- /test/crucible/aws/ecs/task_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs.task-test 2 | (:require [crucible.aws.ecs.task-definition :as task] 3 | [crucible.aws.ecs.container-definition :as container] 4 | [crucible.aws.ecs.secret :as secret] 5 | [crucible.core :refer [xref]] 6 | [clojure.spec.alpha :as s] 7 | [clojure.test :refer :all])) 8 | 9 | (deftest task-tests 10 | 11 | (testing "task with secrets" 12 | (is 13 | (s/valid? ::task/task-definition 14 | {::task/cpu "2048" 15 | ::task/memory "4096" 16 | ::task/container-definitions [{::container/name "rclone" 17 | ::container/image "rclone/rclone" 18 | ::container/secrets [{::secret/name "rclone-access-key" 19 | ::secret/value-from "arn:aws:secretsmanager:region:aws_account_id:secret:value-u9bH6K"}]}]})))) 20 | -------------------------------------------------------------------------------- /src/crucible/aws/events/ecs_parameters.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.events.ecs-parameters 2 | "AWS::ECS::Rule > EcsParameters" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.events.network-configuration :as network-configuration] 5 | [crucible.resources :refer [spec-or-ref]])) 6 | 7 | (s/def ::group (spec-or-ref string?)) 8 | (s/def ::launch-type (spec-or-ref string?)) 9 | (s/def ::platform-version (spec-or-ref string?)) 10 | (s/def ::task-count (spec-or-ref integer?)) 11 | (s/def ::task-definition-arn (spec-or-ref string?)) 12 | (s/def ::network-configuration ::network-configuration/network-configuration-spec) 13 | 14 | (s/def ::ecs-parameters-spec (s/keys :req [::task-definition-arn] 15 | :opt [::group 16 | ::launch-type 17 | ::platform-version 18 | ::task-count 19 | ::network-configuration])) 20 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/layer_version.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.layer-version 2 | (:require [crucible.resources :refer [spec-or-ref defresource]] 3 | [crucible.aws.serverless :as sam] 4 | [clojure.spec.alpha :as s])) 5 | 6 | (s/def ::layer-name (spec-or-ref string?)) 7 | 8 | (s/def ::description (spec-or-ref string?)) 9 | 10 | (s/def ::content-uri (spec-or-ref (s/or :string string? 11 | :s3-location ::sam/s3-location))) 12 | 13 | (s/def ::compatible-runtimes (spec-or-ref (s/* string?))) 14 | 15 | (s/def ::license-info (spec-or-ref string?)) 16 | 17 | (s/def ::retention-policy (spec-or-ref #{"Retain" "Delete"})) 18 | 19 | (s/def ::layer-version 20 | (s/keys :req [::content-uri] 21 | :opt [::layer-name 22 | ::description 23 | ::compatible-runtimes 24 | ::license-info 25 | ::retention-policy])) 26 | 27 | (defresource layer-version "AWS::Serverless::LayerVersion" ::layer-version) 28 | -------------------------------------------------------------------------------- /test/crucible/aws/cloudwatch/anomaly_detector_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.cloudwatch.anomaly-detector-test 2 | (:require [cheshire.core :as json] 3 | [clojure.java.io :as io] 4 | [clojure.test :as t :refer :all] 5 | [crucible.assertion :refer [resource=]] 6 | [crucible.aws.cloudwatch.anomaly-detector :as ad])) 7 | 8 | (deftest anomaly-detection-test 9 | (testing "encode" 10 | (is (resource= 11 | (json/decode (slurp (io/resource "aws/cloudwatch/anomaly_detector.json"))) 12 | (ad/anomaly-detector 13 | {::ad/namespace "AWSSDK/Java" 14 | ::ad/stat "Average" 15 | ::ad/metric-name "JvmMetric" 16 | ::ad/configuration 17 | {::ad/excluded-time-ranges 18 | [{::ad/end-time "2019-07-01T23:59:59" 19 | ::ad/start-time "2019-07-01T23:59:59"}] 20 | ::ad/metric-time-zone "America/New_York"} 21 | ::ad/dimensions [{::ad/name "Memory" 22 | ::ad/value "UsedMemory"}]}))))) 23 | -------------------------------------------------------------------------------- /src/crucible/aws/elbv2.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2 2 | "Resources in AWS::ElasticLoadBalancingV2::*" 3 | (:require [crucible.resources :refer [spec-or-ref defresource] :as res] 4 | [crucible.aws.elbv2.load-balancer :as load-balancer] 5 | [crucible.aws.elbv2.target-group :as target-group] 6 | [crucible.aws.elbv2.listener :as listener] 7 | [crucible.aws.elbv2.listener-rule :as listener-rule] 8 | [crucible.aws.elbv2.listener-certificate :as listener-certificate])) 9 | 10 | (defn prefix [suffix] (str "AWS::ElasticLoadBalancingV2::" suffix)) 11 | 12 | (defresource load-balancer (prefix "LoadBalancer") ::load-balancer/resource-spec) 13 | 14 | (defresource target-group (prefix "TargetGroup") ::target-group/resource-spec) 15 | 16 | (defresource listener (prefix "Listener") ::listener/resource-spec) 17 | 18 | (defresource listener-rule (prefix "ListenerRule") ::listener-rule/resource-spec) 19 | 20 | (defresource listener-certificate (prefix "ListenerCertificate") ::listener-certificate/resource-spec) 21 | -------------------------------------------------------------------------------- /test/crucible/aws/neptune/db_cluster_parameter_group_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-cluster-parameter-group-test 2 | (:require [crucible.aws.neptune.db-cluster-parameter-group :as dbcpg] 3 | [crucible.resources :as r] 4 | [clojure.test :refer :all])) 5 | 6 | (deftest neptune-db-cluster-parameter-group-test 7 | (testing "encode" 8 | (is (= {"Type" "AWS::Neptune::DBClusterParameterGroup", 9 | "Properties" 10 | {"Description" "description", 11 | "Parameters" {"param1" "value1", "param2" "value2"}, 12 | "Family" "neptune1", 13 | "Tags" [{"Key" "key", "Value" "value"}]}} 14 | (crucible.encoding/rewrite-element-data 15 | (dbcpg/db-cluster-parameter-group 16 | {::dbcpg/description "description" 17 | ::dbcpg/parameters {"param1" "value1" 18 | "param2" "value2"} 19 | ::dbcpg/family "neptune1" 20 | ::r/tags [{::r/key "key" 21 | ::r/value "value"}]})))))) 22 | -------------------------------------------------------------------------------- /src/crucible/aws/sns.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.sns 2 | "Resources in AWS::SNS::*" 3 | (:require [crucible.resources :refer [spec-or-ref defresource]] 4 | [crucible.aws.iam :as iam] 5 | [clojure.spec.alpha :as s])) 6 | 7 | (s/def ::display-name (spec-or-ref string?)) 8 | 9 | (s/def ::topic-name (spec-or-ref string?)) 10 | 11 | (s/def ::endpoint (spec-or-ref string?)) 12 | 13 | (s/def ::protocol #{"http" "https" "email" "email-json" "sms" "sqs" "application" "lambda"}) 14 | 15 | (s/def ::subscription (s/coll-of (s/keys :req [::endpoint 16 | ::protocol]))) 17 | 18 | (s/def ::topic (s/keys :opt [::display-name 19 | ::topic-name 20 | ::subscription])) 21 | 22 | (defresource topic "AWS::SNS::Topic" ::topic) 23 | 24 | (s/def ::topics (s/* (spec-or-ref string?))) 25 | 26 | (s/def ::topic-policy (s/keys :req [::iam/policy-document 27 | ::topics])) 28 | 29 | (defresource topic-policy "AWS::SNS::TopicPolicy" ::topic-policy) 30 | -------------------------------------------------------------------------------- /src/crucible/encoding/serverless.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.encoding.serverless 2 | (:require [crucible.encoding.keys :refer [->key]] 3 | [crucible.encoding :as encoding] 4 | [cheshire.core :as json])) 5 | 6 | (defn build 7 | "Create a Serverless Application Model compatible data structure ready for 8 | JSON encoding from the template and any global template values" 9 | ([template] 10 | (build template nil)) 11 | ([template globals] 12 | (-> template 13 | :elements 14 | (#'encoding/elements->template 15 | (cond-> {(->key :aws-template-format-version) "2010-09-09" 16 | (->key :description) (or (:description template) 17 | "No description provided") 18 | (->key :transform) "AWS::Serverless-2016-10-31"} 19 | globals (assoc (->key :globals) 20 | (encoding/rewrite-element-data globals))))))) 21 | 22 | (defn encode 23 | "Convert the template data structure into a JSON-encoded string" 24 | ([template] 25 | (encode template nil)) 26 | ([template globals] 27 | (json/encode (build template globals)))) 28 | -------------------------------------------------------------------------------- /src/crucible/aws/elbv2/listener_rule.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.listener-rule 2 | "AWS::ElasticLoadBalancingV2::Listener" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 5 | 6 | ;; http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listenerrule.html 7 | 8 | (s/def ::target-group-arn (spec-or-ref string?)) 9 | (s/def ::type (spec-or-ref #{"forward"})) 10 | (s/def ::action (s/keys :req [::target-group-arn 11 | ::type])) 12 | (s/def ::actions (s/* ::action)) 13 | (s/def ::field (spec-or-ref #{"host-header" "path-pattern"})) 14 | (s/def ::values (spec-or-ref (s/* string?))) 15 | (s/def ::condition (s/keys :opt [::field 16 | ::values])) 17 | (s/def ::conditions (s/* ::condition)) 18 | (s/def ::listener-arn (spec-or-ref string?)) 19 | (s/def ::priority #(s/int-in-range? 1 50000 %)) 20 | 21 | (s/def ::resource-spec (s/keys :req [::actions 22 | ::conditions 23 | ::listener-arn 24 | ::priority])) 25 | -------------------------------------------------------------------------------- /test/crucible/aws/cloudwatch_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.cloudwatch-test 2 | (:require [crucible.aws.cloudwatch :as cw] 3 | [crucible.assertion :refer [resource=]] 4 | [crucible.core :refer [parameter xref]] 5 | [cheshire.core :as json] 6 | [clojure.test :refer :all :as t] 7 | [clojure.java.io :as io])) 8 | 9 | (deftest minimal-cloudwatch-test 10 | (testing "encode" 11 | (is (resource= 12 | (json/decode (slurp (io/resource "aws/cloudwatch/alarm.json"))) 13 | (cw/alarm {::cw/alarm-description "Scale-up if CPU is greater than 90% for 10 minutes" 14 | ::cw/metric-name "CPUUtilization" 15 | ::cw/namespace "AWS/EC2" 16 | ::cw/statistic "Average" 17 | ::cw/period 300 18 | ::cw/evaluation-periods 2 19 | ::cw/threshold 90.0 20 | ::cw/alarm-actions [(xref :web-server-scale-up-policy)] 21 | ::cw/dimensions [{::cw/name "AutoScalingGroupName" 22 | ::cw/value (xref :web-server-group)}] 23 | ::cw/comparison-operator "GreaterThanThreshold"}))))) 24 | -------------------------------------------------------------------------------- /src/crucible/aws/s3/bucket_encryption.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.s3.bucket-encryption 2 | "Amazon S3 BucketEncrtyption, a property of AWS::S3::Bucket" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.encoding.keys :refer [->key]] 5 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 6 | 7 | (defn kms-master-key-allowed? 8 | [opts] 9 | (if (contains? opts ::kms-master-key-id) 10 | (= "aws:kms" (::sse-algorithm opts)) 11 | true)) 12 | 13 | (s/def ::server-side-encryption-configuration (spec-or-ref (s/coll-of ::server-side-encryption-rule :kind vector?) )) 14 | (s/def ::server-side-encryption-rule (s/keys :opt [::server-side-encryption-by-default])) 15 | (s/def ::server-side-encryption-by-default (s/and 16 | (s/keys :req [::sse-algorithm] :opt [::kms-master-key-id]) 17 | kms-master-key-allowed?)) 18 | (s/def ::sse-algorithm #{"AES256" "aws:kms"}) 19 | (s/def ::kms-master-key-id (spec-or-ref string?)) 20 | 21 | (defmethod ->key :sse-algorithm [_] "SSEAlgorithm") 22 | (defmethod ->key :kms-master-key-id [_] "KMSMasterKeyID") 23 | 24 | (s/def ::resource-property-spec (s/keys :req [::server-side-encryption-configuration])) 25 | -------------------------------------------------------------------------------- /src/crucible/aws/elbv2/listener.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.listener 2 | "AWS::ElasticLoadBalancingV2::Listener" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 5 | 6 | ;; http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html 7 | 8 | (s/def ::target-group-arn (spec-or-ref string?)) 9 | (s/def ::type (spec-or-ref #{"forward"})) 10 | (s/def ::default-action (s/keys :req [::target-group-arn ::type])) 11 | (s/def ::default-actions (s/* ::default-action)) 12 | (s/def ::load-balancer-arn (spec-or-ref string?)) 13 | (s/def ::port (spec-or-ref integer?)) 14 | (s/def ::protocol (spec-or-ref #{"HTTPS" "HTTP" "TCP"})) 15 | (s/def ::ssl-policy (spec-or-ref string?)) 16 | (s/def ::certificate-arn (spec-or-ref string?)) 17 | (s/def ::certificate (s/keys :opt [::certificate-arn])) 18 | (s/def ::certificates (s/* ::certificate)) 19 | 20 | (s/def ::resource-spec (s/keys :req [::default-actions 21 | ::load-balancer-arn 22 | ::port 23 | ::protocol] 24 | :opt [::ssl-policy 25 | ::certificates])) 26 | -------------------------------------------------------------------------------- /test/crucible/encoding/keys_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.encoding.keys-test 2 | (:require [crucible.encoding.keys :as keys] 3 | [crucible.core :as cru] 4 | [crucible.resources :as r] 5 | [clojure.test :refer :all] 6 | [clojure.spec.alpha :as s])) 7 | 8 | (def testing-123-translation "Testing123Foo") 9 | 10 | (defmethod keys/->key :testing-123 [_] testing-123-translation) 11 | 12 | (s/def ::foo (s/keys :req [::testing-123])) 13 | 14 | (def test-resource (r/resource-factory "Test::Test" ::foo)) 15 | 16 | (deftest ->key-translates-on-encode-template 17 | (testing "->key in element key position translates" 18 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 19 | "Description" "t" 20 | "Parameters" {testing-123-translation {"Type" "String"}}} 21 | (cheshire.core/decode 22 | (cru/encode 23 | (cru/template "t" 24 | :testing-123 (cru/parameter))))))) 25 | 26 | (testing "->key in element properties position translates" 27 | (is (get-in (cheshire.core/decode 28 | (cru/encode 29 | (cru/template "t" 30 | :foo (test-resource {::testing-123 "foo"})))) 31 | ["Resources" "Foo" "Properties" "Testing123Foo"])))) 32 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless 2 | (:require [crucible.resources :refer [spec-or-ref]] 3 | [clojure.spec.alpha :as s])) 4 | 5 | ;; ARN 6 | (s/def ::arn (spec-or-ref string?)) 7 | 8 | ;; Variables 9 | (s/def ::variables (s/map-of (s/or :kw keyword? :str string?) (spec-or-ref string?))) 10 | 11 | ;; Tags 12 | (s/def ::tags (s/map-of (s/or :kw keyword? :str string?) (spec-or-ref string?))) 13 | 14 | ;; S3 Location 15 | (s/def ::bucket (spec-or-ref string?)) 16 | 17 | (s/def ::key (spec-or-ref string?)) 18 | 19 | (s/def ::version (spec-or-ref int?)) 20 | 21 | (s/def ::s3-location 22 | (s/keys :req [::bucket 23 | ::key] 24 | :opt [::version])) 25 | ;; CORS 26 | (s/def ::allow-methods (spec-or-ref string?)) 27 | 28 | (s/def ::allow-headers (spec-or-ref string?)) 29 | 30 | (s/def ::allow-origin (spec-or-ref string?)) 31 | 32 | (s/def ::max-age (spec-or-ref string?)) 33 | 34 | (s/def ::cors 35 | (s/keys :req [::allow-origin] 36 | :opt [::allow-methods 37 | ::allow-headers 38 | ::max-age])) 39 | 40 | ;; Primary Key 41 | (s/def ::name (spec-or-ref string?)) 42 | 43 | (s/def ::type (spec-or-ref #{"String" "Number" "Binary"})) 44 | 45 | (s/def ::primary-key 46 | (s/keys :opt [::name 47 | ::type])) 48 | -------------------------------------------------------------------------------- /demo-project/templates/cloudformation/infrastructure.clj: -------------------------------------------------------------------------------- 1 | (ns cloudformation.infrastructure 2 | (:require [crucible.aws.s3 :as s3] 3 | [crucible.core :refer [template xref parameter output]] 4 | [crucible.policies :as policies] 5 | 6 | ;; require the myproject.hello ns to ensure that it is loaded before this ns. 7 | ;; is there a neater way to make sure the ns is correct in the :bucket-name param? 8 | ;; PRs/explanations welcome! 9 | [myproject.hello])) 10 | 11 | 12 | 13 | (def my-template 14 | (template "A simple demo template" 15 | 16 | ;; use the namespace of myproject.hello to define the bucket name 17 | :bucket-name (parameter :default (str (-> 'myproject.hello the-ns str) "-repo")) 18 | 19 | ;; create a bucket with website hosting enabled 20 | :bucket (s3/bucket {::s3/access-control "PublicRead" 21 | ::s3/website-configuration {::s3/index-document "index.html" 22 | ::s3/error-document "error.html"}} 23 | (policies/deletion ::policies/retain)) 24 | 25 | ;; output the domain name of the s3 bucket website 26 | :website-domain (output (xref :bucket :domain-name)))) 27 | -------------------------------------------------------------------------------- /src/crucible/aws/ec2/route.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.route 2 | "AWS::EC2::Route" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource]] 5 | [crucible.aws.ec2 :as ec2])) 6 | 7 | (s/def ::route-table-id (spec-or-ref string?)) 8 | 9 | (s/def ::destination-cidr-block (spec-or-ref string?)) 10 | 11 | (s/def ::destination-ipv6-cidr-block (spec-or-ref string?)) 12 | 13 | (s/def ::egress-only-internet-gateway-id (spec-or-ref string?)) 14 | 15 | (s/def ::gateway-id (spec-or-ref string?)) 16 | 17 | (s/def ::instance-id (spec-or-ref string?)) 18 | 19 | (s/def ::nat-gateway-id (spec-or-ref string?)) 20 | 21 | (s/def ::network-interface-id (spec-or-ref string?)) 22 | 23 | (s/def ::vpc-peering-connection-id (spec-or-ref string?)) 24 | 25 | (s/def ::route (s/keys :req [::route-table-id] 26 | :opt [::destination-cidr-block 27 | ::destination-ipv6-cidr-block 28 | ::egress-only-internet-gateway-id 29 | ::gateway-id 30 | ::instance-id 31 | ::nat-gateway-id 32 | ::network-interface-id 33 | ::vpc-peering-connection-id])) 34 | 35 | (defresource route (ec2/ec2 "Route") ::route) 36 | -------------------------------------------------------------------------------- /test/crucible/aws/serverless/layer_version_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.layer-version-test 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.aws.serverless.layer-version :as lv] 4 | [crucible.core :refer [xref]] 5 | [crucible.resources :as res] 6 | [clojure.test :refer :all] 7 | [crucible.encoding :as encoding])) 8 | 9 | (deftest layer-version-test 10 | (testing "encoding" 11 | (is (= {"Type" "AWS::Serverless::LayerVersion" 12 | "Properties" 13 | {"LayerName" "MyLayer" 14 | "Description" "Layer description" 15 | "ContentUri" "s3://my-bucket/my-layer.zip" 16 | "CompatibleRuntimes" ["nodejs6.10" 17 | "nodejs8.10"] 18 | "LicenseInfo" "Available under the MIT-0 license." 19 | "RetentionPolicy" "Retain"}} 20 | (encoding/rewrite-element-data 21 | (lv/layer-version 22 | {::lv/layer-name "MyLayer" 23 | ::lv/description "Layer description" 24 | ::lv/content-uri "s3://my-bucket/my-layer.zip" 25 | ::lv/compatible-runtimes ["nodejs6.10" 26 | "nodejs8.10"] 27 | ::lv/license-info "Available under the MIT-0 license." 28 | ::lv/retention-policy "Retain"})))))) 29 | -------------------------------------------------------------------------------- /test/crucible/aws/events_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.events-test 2 | (:require [crucible.aws.events :as events] 3 | [crucible.assertion :refer [resource=]] 4 | [crucible.core :refer [parameter xref]] 5 | [cheshire.core :as json] 6 | [clojure.test :refer :all :as t] 7 | [clojure.java.io :as io])) 8 | 9 | (deftest minimal-events-test 10 | (testing "encode" 11 | (is (= 12 | (json/decode (slurp (io/resource "aws/events/simple-event.json"))) 13 | (crucible.encoding/rewrite-element-data 14 | (events/rule {::events/description "EventRule" 15 | ::events/event-pattern {"detail-type" 16 | [ "AWS API Call via CloudTrail" ] 17 | "detail" {"userIdentity" {"type" ["Root"]}}} 18 | ::events/state "ENABLED" 19 | ::events/targets [{::events/arn (xref :my-sns-topic) 20 | ::events/id "OpsTopic"}]})))))) 21 | 22 | (deftest target-id-regex-test 23 | (testing "target ID regex is required" 24 | (is (thrown? Exception 25 | (events/rule {::events/targets [{::events/arn (xref :my-sns-topic) 26 | ::events/id "foo bar"}]})) 27 | "Invalid target ID should throw exception"))) 28 | -------------------------------------------------------------------------------- /src/crucible/aws/auto_scaling/launch_configuration.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.auto-scaling.launch-configuration 2 | "AWS::AutoScaling::LaunchConfiguration" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref] :as res])) 5 | 6 | (s/def ::associate-public-ip-address (spec-or-ref boolean?)) 7 | (s/def ::iam-instance-profile (spec-or-ref string?)) 8 | (s/def ::image-id (spec-or-ref string?)) 9 | (s/def ::instance-id (spec-or-ref string?)) 10 | (s/def ::instance-monitoring (spec-or-ref boolean?)) 11 | (s/def ::instance-type (spec-or-ref string?)) 12 | (s/def ::kernel-id (spec-or-ref string?)) 13 | (s/def ::key-name (spec-or-ref string?)) 14 | (s/def ::security-group (spec-or-ref string?)) 15 | (s/def ::security-groups (s/* ::security-group)) 16 | (s/def ::user-data (spec-or-ref string?)) 17 | 18 | (s/def ::resource-spec (s/keys :req [::image-id 19 | ::instance-type] 20 | :opt [::associate-public-ip-address 21 | ::iam-instance-profile 22 | ::instance-id 23 | ::instance-monitoring 24 | ::kernel-id 25 | ::key-name 26 | ::security-groups 27 | ::user-data])) 28 | -------------------------------------------------------------------------------- /src/crucible/aws/events.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.events 2 | "Resources in AWS::Events::*" 3 | (:require [crucible.resources :refer [spec-or-ref defresource] :as res] 4 | [crucible.values :as v] 5 | [crucible.encoding.keys :refer [->key]] 6 | [crucible.aws.events.ecs-parameters :as ecs-parameters] 7 | [clojure.spec.alpha :as s])) 8 | 9 | (s/def ::id (spec-or-ref (s/and string? #(re-matches #"[\.\-_A-Za-z0-9]+" %)))) 10 | 11 | (s/def ::arn (spec-or-ref string?)) 12 | 13 | (s/def ::ecs-parameters ::ecs-parameters/ecs-parameters-spec) 14 | 15 | (s/def ::target (s/keys :req [::arn 16 | ::id] 17 | :opt [::ecs-parameters])) 18 | 19 | (s/def ::targets (s/coll-of ::target :kind vector?)) 20 | 21 | (s/def ::state (spec-or-ref #{"ENABLED" "DISABLED"})) 22 | 23 | (s/def ::schedule-expression (spec-or-ref string?)) 24 | 25 | (s/def ::role-arn (spec-or-ref string?)) 26 | 27 | (s/def ::event-pattern (spec-or-ref any?)) 28 | 29 | (s/def ::name (spec-or-ref string?)) 30 | 31 | (s/def ::description (spec-or-ref string?)) 32 | 33 | (s/def ::rule (s/keys :opt [::description 34 | ::event-pattern 35 | ::name 36 | ::role-arn 37 | ::schedule-expression 38 | ::state 39 | ::targets])) 40 | 41 | (defresource rule "AWS::Events::Rule" ::rule) 42 | -------------------------------------------------------------------------------- /test-resources/aws/s3/s3-cors.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::S3::Bucket", 3 | "Properties": { 4 | "AccessControl": "PublicReadWrite", 5 | "CorsConfiguration": { 6 | "CorsRules": [ 7 | { 8 | "AllowedHeaders": [ 9 | "*" 10 | ], 11 | "AllowedMethods": [ 12 | "GET" 13 | ], 14 | "AllowedOrigins": [ 15 | "*" 16 | ], 17 | "ExposedHeaders": [ 18 | "Date" 19 | ], 20 | "Id": "myCORSRuleId1", 21 | "MaxAge": 3600 22 | }, 23 | { 24 | "AllowedHeaders": [ 25 | "x-amz-*" 26 | ], 27 | "AllowedMethods": [ 28 | "DELETE" 29 | ], 30 | "AllowedOrigins": [ 31 | "http://www.example1.com", 32 | "http://www.example2.com" 33 | ], 34 | "ExposedHeaders": [ 35 | "Connection", 36 | "Server", 37 | "Date" 38 | ], 39 | "Id": "myCORSRuleId2", 40 | "MaxAge": 1800 41 | } 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/crucible/parameters.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.parameters 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.encoding.keys :as keys])) 4 | 5 | (defmethod keys/->key :list-number [_] "List") 6 | (defmethod keys/->key :aws-ec2-az-name [_] "AWS::EC2::AvailabilityZone::Name") 7 | (defmethod keys/->key :aws-ec2-image-id [_] "AWS::EC2::Image::Id") 8 | (defmethod keys/->key :aws-ec2-instance-id [_] "AWS::EC2::Instance::Id") 9 | 10 | (s/def ::type #{::string 11 | ::number 12 | ::comma-delimited-list 13 | ::list-number 14 | ::aws-ec2-az-name 15 | ::aws-ec2-image-id 16 | ::aws-ec2-instance-id}) 17 | 18 | (s/def ::description string?) 19 | 20 | (s/def ::constraint-description string?) 21 | 22 | (s/def ::allowed-values (s/+ string?)) 23 | 24 | (s/def ::allowed-pattern string?) 25 | 26 | (s/def ::default string?) 27 | 28 | (s/def ::no-echo #{true}) 29 | 30 | (s/def ::max-value (s/and number? pos?)) 31 | 32 | (s/def ::min-value ::max-value) 33 | 34 | (s/def ::max-length (s/and integer? pos?)) 35 | 36 | (s/def ::min-length ::max-length) 37 | 38 | (s/def ::parameter 39 | (s/keys :req [::type] 40 | :opt [::description 41 | ::allowed-values 42 | ::allowed-pattern 43 | ::constraint-description 44 | ::default 45 | ::no-echo 46 | ::min-value 47 | ::max-value 48 | ::min-length 49 | ::max-length])) 50 | -------------------------------------------------------------------------------- /test/crucible/aws/serverless/simple_table_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.simple-table-test 2 | (:require [crucible.aws.dynamodb :as ddb] 3 | [crucible.aws.serverless :as sam] 4 | [crucible.aws.serverless.simple-table :as st] 5 | [crucible.core :refer [xref]] 6 | [crucible.resources :as res] 7 | [clojure.test :refer :all] 8 | [crucible.encoding :as encoding])) 9 | 10 | (deftest simple-table-test 11 | (testing "encode" 12 | (is (= {"Type" "AWS::Serverless::SimpleTable" 13 | "Properties" 14 | {"TableName" "my-table" 15 | "PrimaryKey" {"Name" "id" 16 | "Type" "String"} 17 | "ProvisionedThroughput" {"ReadCapacityUnits" 5 18 | "WriteCapacityUnits" 5} 19 | "Tags" {"Department" "Engineering" 20 | "AppType" "Serverless"} 21 | "SseSpecification" {"SseEnabled" true}}} 22 | (encoding/rewrite-element-data 23 | (st/simple-table 24 | {::st/table-name "my-table" 25 | ::st/primary-key {::sam/name "id" 26 | ::sam/type "String"} 27 | ::st/provisioned-throughput {::ddb/read-capacity-units 5 28 | ::ddb/write-capacity-units 5} 29 | ::st/tags {:department "Engineering" 30 | :app-type "Serverless"} 31 | ::st/sse-specification {::ddb/sse-enabled true}})))))) 32 | -------------------------------------------------------------------------------- /src/crucible/aws/sqs/queue.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.sqs.queue 2 | "AWS::SQS::Queue" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res] 5 | [crucible.aws.sqs.redrive-policy :as redrive-policy])) 6 | 7 | (s/def ::content-based-deduplication (spec-or-ref boolean?)) 8 | (s/def ::delay-seconds (spec-or-ref integer?)) 9 | (s/def ::fifo-queue (spec-or-ref boolean?)) 10 | (s/def ::kms-master-key-id (spec-or-ref string?)) 11 | (s/def ::maximum-message-size (spec-or-ref integer?)) 12 | (s/def ::message-retention-period (spec-or-ref integer?)) 13 | (s/def ::queue-name (spec-or-ref string?)) 14 | (s/def ::receive-message-wait-time-seconds (spec-or-ref integer?)) 15 | (s/def ::redrive-policy (spec-or-ref ::redrive-policy/resource-property-spec)) 16 | (s/def ::visibility-timeout (spec-or-ref integer?)) 17 | 18 | (s/def ::resource-spec (s/keys :opt [::content-based-deduplication 19 | ::delay-seconds 20 | ::fifo-queue 21 | ::kms-master-key-id 22 | ::kms-data-key-reuse-period-sec 23 | ::maximum-message-size 24 | ::message-retention-period 25 | ::queue-name 26 | ::receive-message-wait-time-seconds 27 | ::redrive-policy 28 | ::visibility-timeout])) 29 | -------------------------------------------------------------------------------- /src/crucible/aws/ec2/security_group_ingress.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2.security-group-ingress 2 | "AWS::EC2::SecurityGroupIngress" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource]] 5 | [crucible.aws.ec2 :as ec2])) 6 | 7 | (s/def ::ip-protocol (spec-or-ref (s/or :int (s/and integer? 8 | #(<= -1 %)) 9 | :str #{"tcp" "udp" "icmp"}))) 10 | 11 | (s/def ::cidr-ip (spec-or-ref string?)) 12 | 13 | (s/def ::cidr-ipv6 (spec-or-ref string?)) 14 | 15 | (s/def ::description (spec-or-ref string?)) 16 | 17 | (s/def ::from-port ::ec2/port) 18 | 19 | (s/def ::to-port ::ec2/port) 20 | 21 | (s/def ::security-group-ingress (s/keys :req [::ip-protocol] 22 | :opt [::cidr-ip 23 | ::cidr-ipv6 24 | ::description 25 | ::from-port 26 | ::to-port 27 | ::group-id 28 | ::group-name 29 | ::source-security-group-id 30 | ::source-security-group-name 31 | ::source-security-group-owner-id])) 32 | 33 | (defresource security-group-ingress (ec2/ec2 "SecurityGroupIngress") ::security-group-ingress) 34 | -------------------------------------------------------------------------------- /src/crucible/aws/cloudwatch/anomaly_detector.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.cloudwatch.anomaly-detector 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.aws.cloudwatch :as cloudwatch] 4 | [crucible.encoding.keys :refer [->key]] 5 | [crucible.resources :refer [defresource spec-or-ref]])) 6 | 7 | (s/def ::value (spec-or-ref string?)) 8 | 9 | (s/def ::name (spec-or-ref string?)) 10 | 11 | (s/def ::dimension (s/keys :req [::name ::value])) 12 | 13 | (s/def ::dimensions (s/coll-of ::dimension)) 14 | 15 | (s/def ::stat ::cloudwatch/statistic) 16 | 17 | (def date-time-regex 18 | #"(19|20)[0-9][0-9]-(0[0-9]|1[0-2])-(0[1-9]|([12][0-9]|3[01]))T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]") 19 | 20 | (s/def ::date-time (spec-or-ref (s/and string? #(re-matches date-time-regex %)))) 21 | 22 | (s/def ::end-time ::date-time) 23 | 24 | (s/def ::start-time ::date-time) 25 | 26 | (s/def ::range (s/keys :req [::start-time ::end-time])) 27 | 28 | (s/def ::ranges (s/coll-of ::range)) 29 | 30 | (s/def ::namespace (spec-or-ref string?)) 31 | 32 | (s/def ::metric-name (spec-or-ref string?)) 33 | 34 | (s/def ::excluded-time-ranges (spec-or-ref ::ranges)) 35 | 36 | (s/def ::metric-time-zone (spec-or-ref string?)) 37 | 38 | (s/def ::configuration (s/keys :opt [::excluded-time-ranges ::metric-time-zone])) 39 | 40 | (s/def ::anomaly-detector (s/keys :req [::metric-name 41 | ::namespace 42 | ::stat])) 43 | 44 | (defresource anomaly-detector "AWS::CloudWatch::AnomalyDetector" ::anomaly-detector) 45 | -------------------------------------------------------------------------------- /test/crucible/aws/ecs/service_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs.service-test 2 | (:require [crucible.aws.ecs :as ecs] 3 | [crucible.aws.ecs.service :as service] 4 | [crucible.core :refer [xref]] 5 | [clojure.spec.alpha :as s] 6 | [clojure.test :refer :all])) 7 | 8 | (deftest service-tests 9 | 10 | (testing "valid service" 11 | (is 12 | (s/valid? ::service/service 13 | {::service/service-name "https-redirect-v2" 14 | ::service/platform-version "1.1.0" 15 | ::service/task-definition (xref :https-redirect-task) 16 | ::service/cluster (xref :cluster) 17 | ::service/launch-type "FARGATE" 18 | ::service/network-configuration {::service/aws-vpc-configuration 19 | {::service/subnets [(xref :private1) 20 | (xref :private2)] 21 | ::service/security-groups [(xref :sg-private) 22 | (xref :sg-https-redirect)] 23 | ::service/assign-public-ip "DISABLED"} } 24 | ::service/load-balancers [{::service/container-name "https-redirect" 25 | ::service/container-port 80 26 | ::service/target-group-arn (xref :http-target-group)}] 27 | ::service/desired-count 1})))) 28 | -------------------------------------------------------------------------------- /src/crucible/aws/neptune/db_instance.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-instance 2 | "Specs for AWS::Neptune::DBInstance" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.neptune :as neptune] 5 | [crucible.encoding.keys :refer [->key]] 6 | [crucible.resources :refer [spec-or-ref defresource]])) 7 | 8 | (s/def ::allow-major-version-upgrade (spec-or-ref boolean?)) 9 | 10 | (s/def ::auto-minor-version-upgrade (spec-or-ref boolean?)) 11 | 12 | (s/def ::availability-zone (spec-or-ref string?)) 13 | 14 | (s/def ::db-cluster-identifier ::neptune/db-cluster-identifier) 15 | 16 | (s/def ::db-instance-class (spec-or-ref string?)) 17 | (defmethod ->key :db-instance-class [_] "DBInstanceClass") 18 | 19 | (s/def ::db-instance-identifier (spec-or-ref string?)) 20 | (defmethod ->key :db-instance-identifier [_] "DBInstanceIdentifier") 21 | 22 | (s/def ::db-parameter-group-name (spec-or-ref string?)) 23 | (defmethod ->key :db-parameter-group-name [_] "DBParameterGroupName") 24 | 25 | (s/def ::db-subnet-group-name ::neptune/db-subnet-group-name) 26 | 27 | (s/def ::preferred-maintenance-window (spec-or-ref string?)) 28 | 29 | (s/def ::db-instance 30 | (s/keys :req [::db-instance-class] 31 | :opt [::allow-major-version-upgrade 32 | ::auto-minor-version-upgrade 33 | ::availability-zone 34 | ::db-cluster-identifier 35 | ::db-instance-identifier 36 | ::db-parameter-group-name 37 | ::db-subnet-group-name 38 | ::preferred-maintenance-window 39 | :crucible.resources/tags])) 40 | 41 | (defresource db-instance "AWS::Neptune::DBInstance" ::db-instance) 42 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/api.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.api 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.resources :refer [spec-or-ref defresource]] 4 | [clojure.spec.alpha :as s])) 5 | 6 | (s/def ::name (spec-or-ref string?)) 7 | 8 | (s/def ::stage-name (spec-or-ref string?)) 9 | 10 | (s/def ::definition-uri (spec-or-ref (s/or :string string? 11 | :s3-location ::sam/s3-location))) 12 | 13 | (s/def ::definition-body map?) 14 | 15 | (s/def ::cache-clustering-enabled (spec-or-ref boolean?)) 16 | 17 | (s/def ::cache-cluster-size (spec-or-ref string?)) 18 | 19 | (s/def ::variables ::sam/variables) 20 | 21 | (s/def ::http-method (spec-or-ref string?)) 22 | 23 | (s/def ::resource-path (spec-or-ref string?)) 24 | 25 | (s/def ::method-setting 26 | (s/keys :req [::http-method 27 | ::resource-path])) 28 | 29 | (s/def ::method-settings (s/* ::method-setting)) 30 | 31 | (s/def ::endpoint-configuration #{"REGIONAL" "EDGE" "PRIVATE"}) 32 | 33 | (s/def ::binary-media-types (s/* (spec-or-ref string?))) 34 | 35 | (s/def ::cors (spec-or-ref (s/or :domain string? 36 | :cors-configuration ::sam/cors))) 37 | 38 | (s/def ::api 39 | (s/keys :req [::stage-name] 40 | :opt [::name 41 | ::definition-uri 42 | ::definition-body 43 | ::cache-clustering-enabled 44 | ::cache-cluster-size 45 | ::variables 46 | ::method-settings 47 | ::endpoint-configuration 48 | ::binary-media-types 49 | ::cors])) 50 | 51 | (defresource api "AWS::Serverless::Api" ::api) 52 | -------------------------------------------------------------------------------- /src/crucible/aws/elbv2/load_balancer.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.load-balancer 2 | "AWS::ElasticLoadBalancingV2::LoadBalancer" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 5 | 6 | ;; http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html 7 | 8 | (s/def ::key string?) 9 | (s/def ::value (spec-or-ref string?)) 10 | (s/def ::name (spec-or-ref string?)) 11 | (s/def ::scheme (spec-or-ref #{"internet-facing" "internal"})) 12 | (s/def ::type (spec-or-ref #{"application" "network"})) 13 | (s/def ::ip-address-type (spec-or-ref #{"ipv4" "dualstack"})) 14 | (s/def ::load-balancer-attribute (s/keys :opt [::key ::value])) 15 | (s/def ::load-balancer-attributes (s/* ::load-balancer-attribute)) 16 | (s/def ::security-group-id (spec-or-ref string?)) 17 | (s/def ::security-groups (s/* ::security-group-id)) 18 | (s/def ::subnet-id (spec-or-ref string?)) 19 | (s/def ::allocation-id (spec-or-ref string?)) 20 | (s/def ::subnet-mapping (s/keys :req [::subnet-id 21 | ::allocation-id])) 22 | (s/def ::subnet-mappings (s/* ::subnet-mapping)) 23 | (s/def ::subnet (spec-or-ref string?)) 24 | (s/def ::subnets (s/* ::subnet)) 25 | (s/def ::resource-spec (s/keys :opt [::load-balancer-attributes 26 | ::name 27 | ::scheme 28 | ::security-groups 29 | ::subnet-mappings 30 | ::subnets 31 | ::res/tags 32 | ::type 33 | ::ip-address-type])) 34 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function/event_source.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function.event-source 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.aws.serverless.function.event-source.api :as api] 4 | [crucible.aws.serverless.function.event-source.kinesis :as kinesis] 5 | [crucible.aws.serverless.function.event-source.schedule :as schedule] 6 | [crucible.aws.serverless.function.event-source.sns :as sns] 7 | [crucible.aws.serverless.function.event-source.sqs :as sqs] 8 | [crucible.resources :refer [spec-or-ref defresource]] 9 | [clojure.spec.alpha :as s])) 10 | 11 | (s/def ::type #{"AlexaSkill" 12 | "Api" 13 | "CloudWatchEvent" 14 | "CloudWatchLogs" 15 | "DynamoDB" 16 | "IoTRule" 17 | "Kinesis" 18 | "S3" 19 | "Schedule" 20 | "SNS" 21 | "SQS"}) 22 | 23 | (defmulti event-source ::type) 24 | 25 | (defmethod event-source "Kinesis" [_] 26 | (s/keys :req [::type 27 | ::kinesis/properties])) 28 | 29 | (defmethod event-source "Schedule" [_] 30 | (s/keys :req [::type 31 | ::schedule/properties])) 32 | 33 | (defmethod event-source "Api" [_] 34 | (s/keys :req [::type 35 | ::api/properties])) 36 | 37 | (defmethod event-source "SNS" [_] 38 | (s/keys :req [::type 39 | ::sns/properties])) 40 | 41 | (defmethod event-source "SQS" [_] 42 | (s/keys :req [::type 43 | ::sqs/properties])) 44 | 45 | (defmethod event-source :default [_] 46 | (s/keys :req [::type])) 47 | 48 | (s/def ::event-source 49 | (s/multi-spec event-source ::type)) 50 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/globals.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.globals 2 | (:require [crucible.aws.serverless.api :as sam.api] 3 | [crucible.aws.serverless.function :as sam.function] 4 | [clojure.spec.alpha :as s] 5 | [expound.alpha :as expound])) 6 | 7 | (s/def ::function (s/keys :opt [::sam.function/handler 8 | ::sam.function/runtime 9 | ::sam.function/code-uri 10 | ::sam.function/description 11 | ::sam.function/memory-size 12 | ::sam.function/timeout 13 | ::sam.function/vpc-config 14 | ::sam.function/environment 15 | ::sam.function/tags])) 16 | 17 | (s/def ::api (s/keys :opt [::sam.api/name 18 | ::sam.api/definition-uri 19 | ::sam.api/cache-clustering-enabled 20 | ::sam.api/cache-cluster-size 21 | ::sam.api/variables 22 | ::sam.api/endpoint-configuration 23 | ::sam.api/method-settings 24 | ::sam.api/binary-media-types 25 | ::sam.api/cors])) 26 | 27 | (s/def ::globals (s/keys :opt [::function 28 | ::api])) 29 | 30 | (defn globals 31 | "Validates Serverless Application Model globals returning a vector of 32 | :globals and the conformed data" 33 | [props] 34 | (if-not (s/valid? ::globals props) 35 | (throw (ex-info (str "Invalid globals property" 36 | (expound/expound-str ::globals props)) 37 | (s/explain-data ::globals props))) 38 | [:globals props])) 39 | -------------------------------------------------------------------------------- /src/crucible/aws/api_gateway.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.api-gateway 2 | "Resources in AWS::ApiGateway::*" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res] 5 | [crucible.encoding.keys :refer [->key]])) 6 | 7 | (defn apigw [resource] (str "AWS::ApiGateway::" (->key resource))) 8 | 9 | (s/def ::rest-api (s/keys :opt [::body 10 | ::body-s3-location 11 | ::clone-from 12 | ::description 13 | ::fail-on-warnings 14 | ::name 15 | ::parameters])) 16 | 17 | (s/def ::account any?) 18 | 19 | (defresource account (apigw :account) ::account) 20 | 21 | (s/def ::api-key any?) 22 | 23 | (defresource api-key (apigw :api-key) ::api-key) 24 | 25 | (s/def ::authorizer any?) 26 | 27 | (defresource authorizer (apigw :authorizer) ::authorizer) 28 | 29 | (s/def ::base-path-mapping any?) 30 | 31 | (defresource base-path-mapping (apigw :base-path-napping) ::base-path-mapping) 32 | 33 | (s/def ::client-certificate any?) 34 | 35 | (defresource client-certificate (apigw :client-certificate) ::client-certificate) 36 | 37 | (s/def ::deployment any?) 38 | 39 | (defresource deployment (apigw :deployment) ::deployment) 40 | 41 | (s/def ::method any?) 42 | 43 | (defresource method (apigw :method) ::method) 44 | 45 | (s/def ::model any?) 46 | 47 | (defresource model (apigw :model) ::model) 48 | 49 | (s/def ::resource any?) 50 | 51 | (defresource resource (apigw :resource) ::resource) 52 | 53 | (s/def ::rest-api any?) 54 | 55 | (defresource rest-api (apigw :rest-api) ::rest-api) 56 | 57 | (s/def ::stage any?) 58 | 59 | (defresource stage (apigw :stage) ::stage) 60 | 61 | (s/def ::usage-plan any?) 62 | 63 | (defresource usage-plan (apigw :usage-plan) ::usage-plan) -------------------------------------------------------------------------------- /src/crucible/aws/ecs/task_definition.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs.task-definition 2 | "Resources in AWS::ECS::TaskDefinition" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref]] 5 | [crucible.aws.ecs.container-definition :as cd])) 6 | 7 | (s/def ::container-definitions (s/coll-of ::cd/container-definition :kind vector?)) 8 | 9 | (s/def ::cpu (spec-or-ref string?)) 10 | 11 | (s/def ::execution-role-arn (spec-or-ref string?)) 12 | 13 | (s/def ::family (spec-or-ref string?)) 14 | 15 | (s/def ::memory (spec-or-ref string?)) 16 | 17 | (s/def ::network-mode #{"bridge" "host" "awsvpc" "none"}) 18 | 19 | (s/def ::placement-constraints-type #{"distinctInstance" "memberOf"}) 20 | (s/def ::placement-constraints-expression (spec-or-ref string?)) 21 | 22 | (s/def ::placement-constraints (s/keys :req [::placement-constraints-type] 23 | :opt [::placement-constraints-expression])) 24 | 25 | (s/def ::requires-compatibilities (s/coll-of #{"EC2" "FARGATE"} :kind vector?)) 26 | 27 | (s/def ::task-role-arn (spec-or-ref string?)) 28 | 29 | (s/def ::name (spec-or-ref string?)) 30 | (s/def ::host (spec-or-ref string?)) 31 | 32 | (s/def ::task-definition-volume (s/keys :req [::name] 33 | :opt [::host])) 34 | 35 | (s/def ::volumes (s/coll-of ::task-definition-volume :kind vector?)) 36 | 37 | (s/def ::task-definition (s/keys :req [::container-definitions] 38 | :opt [::cpu 39 | ::execution-role-arn 40 | ::family 41 | ::memory 42 | ::network-mode 43 | ::placement-constraints 44 | ::requires-compatibilities 45 | ::task-role-arn 46 | ::volumes])) 47 | -------------------------------------------------------------------------------- /test/crucible/resources_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.resources-test 2 | (:require [crucible.resources :as res] 3 | [crucible.values :as v] 4 | [clojure.test :refer :all] 5 | [clojure.spec.alpha :as s])) 6 | 7 | (deftest resource-property-type 8 | (testing "single ref is valid" 9 | (is (s/valid? ::res/resource-property-value (v/xref :foo)))) 10 | 11 | (testing "function is valid" 12 | (is (s/valid? ::res/resource-property-value (v/join "-" ["foo"]))) 13 | (is (s/valid? ::res/resource-property-value (v/find-in-map :foo-map "bar" "baz"))))) 14 | 15 | (deftest resource-factory 16 | (testing "exception thrown if type does not look like a valid AWS resource type" 17 | (is (thrown? Exception (res/resource-factory "bob" ::foo)))) 18 | 19 | (s/def ::foo (s/keys)) 20 | 21 | (testing "factory function places type in ::type key" 22 | (let [type "AWS::Bob"] 23 | (is (= type 24 | (-> type 25 | (res/resource-factory ::foo) 26 | (apply [{}]) 27 | second 28 | ::res/type)))))) 29 | 30 | (deftest resource-factory-validation 31 | (let [type "Custom::MyResource" 32 | spec (s/keys :req [::foo]) 33 | my-resource (res/resource-factory type spec)] 34 | 35 | (testing "resource factory throws on invalid props" 36 | (is (thrown? Exception (my-resource {})))) 37 | 38 | (testing "resource factory constructs element on valid props" 39 | (is (= [:resource {::res/type type ::res/properties {::foo {}}}] 40 | (my-resource {::foo {}})))))) 41 | 42 | (s/def ::meta-test-resource-spec any?) 43 | (res/defresource meta-test-resource "AWS::Meta::Test" ::meta-test-resource-spec) 44 | 45 | (deftest documentation-meta-test 46 | (testing "documentation is added" 47 | (is (-> #'meta-test-resource meta :doc))) 48 | (testing "documentation mentions AWS type" 49 | (is (.contains (-> #'meta-test-resource meta :doc) "AWS::Meta::Test")))) 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via an issue. 4 | That will save you spending time on something that won't be accepted. 5 | Changes that break existing functionality will need especially careful consideration and may be rejected. 6 | 7 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 8 | 9 | ## Contributors: Pull Request Process 10 | 11 | 1. Please ensure new code produces a valid CloudFormation template. 12 | 1. Update [README.md](README.md) if you're adding something that could be useful to new folks or people browsing. 13 | 1. Create a pull request and a maintainer will take a look as soon as possible. You can see the process we follow below. 14 | 15 | If you'd like to be a maintainer after making a couple of contributions, 16 | please mention that on your PR or a comment to your PR. 17 | 18 | ## Maintainers: Merging and Releasing 19 | 20 | ### Current maintainers: 21 | - [brabster](https://github.com/brabster) 22 | - [keerts](https://github.com/keerts) 23 | - [shooit](https://github.com/shooit) 24 | 25 | 1. Check that there's some kind of test coverage for code changes. 26 | The minimum is just to check that the new code produces a template without error. 27 | The build will check that if there's a test. 28 | 1. Ensure that the updates don't alter any existing functionality. 29 | Ask yourself whether any existing template could be broken by the change. 30 | 1. If no problems, merge the PR and ensure the build is successful. 31 | 1. If merge OK, draft a new release in the same form as previous releases and publish it. 32 | Remember to thank the contributor. 33 | The versioning scheme we use just increments the semver minor or patch versions as you think appropriate. 34 | 35 | If the change doesn't pass the test coverage or breaking change checks, 36 | use comments and/or code review to negotiate changes with the maintainer. 37 | -------------------------------------------------------------------------------- /test/crucible/values_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.values-test 2 | (:require [crucible 3 | [core :as cru] 4 | [values :as v]] 5 | [clojure.test :refer :all] 6 | [clojure.spec.alpha :as s])) 7 | 8 | (deftest join-test 9 | (testing "no values" (is (s/valid? ::v/value (cru/join "-" [])))) 10 | (testing "single literal" (is (s/valid? ::v/value (cru/join "-" ["foo"])))) 11 | (testing "single ref" (is (s/valid? ::v/value (cru/join "-" [(cru/xref :foo)])))) 12 | (testing "multiple mixed" (is (s/valid? ::v/value (cru/join "-" ["foo" (cru/xref :foo)])))) 13 | (testing "no delimiter" (is (s/valid? ::v/value (cru/join ["foo" (cru/xref :foo)]))))) 14 | 15 | (deftest if-test 16 | (testing "no values" (is (s/valid? ::v/value (cru/fn-if "-" "a" "b"))))) 17 | 18 | (deftest select-test 19 | (testing "no values" (is (s/valid? ::v/value (cru/select 0 [])))) 20 | (testing "single literal" (is (s/valid? ::v/value (cru/select 0 ["foo"])))) 21 | (testing "single ref" (is (s/valid? ::v/value (cru/select 0 [(cru/xref :foo)])))) 22 | (testing "multiple mixed" (is (s/valid? ::v/value (cru/select 0 ["foo" (cru/xref :foo)])))) 23 | 24 | (testing "1-index" (is (s/valid? ::v/value (cru/select 1 ["foo" (cru/xref :foo)]))))) 25 | 26 | (deftest find-in-map-test 27 | (testing "all literals" (is (s/valid? ::v/value (cru/find-in-map :foo "bar" "baz")))) 28 | (testing "both refs" (is (s/valid? ::v/value (cru/find-in-map :foo 29 | (cru/xref :bar) 30 | (cru/xref :baz))))) 31 | (testing "mixed" (is (s/valid? ::v/value (cru/find-in-map :foo "bar" (cru/xref :baz)))))) 32 | 33 | (deftest import-test 34 | (testing "import name" (is (s/valid? ::v/value (cru/import-value "foo"))))) 35 | 36 | (deftest sub-test 37 | (testing "substitution string" (is (s/valid? ::v/value (cru/sub "${foo} bar"))))) 38 | 39 | (deftest base64-test 40 | (testing "base64 fn" (is (s/valid? ::v/base64 (cru/base64 "hello"))))) 41 | -------------------------------------------------------------------------------- /test/crucible/encoding/values_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.encoding.values-test 2 | (:require [clojure.test :refer :all] 3 | [crucible.values :refer :all])) 4 | 5 | (deftest test-values 6 | 7 | (testing "string value" 8 | (is (= "foo" (encode-value "foo")))) 9 | 10 | (testing "numeric value" 11 | (is (= 4 (encode-value 4)))) 12 | 13 | (testing "ref value" 14 | (is (= {"Ref" "Foo"} (encode-value (xref :foo))))) 15 | 16 | (testing "select fn get-att value" 17 | (is (= {"Fn::GetAtt" ["Resource" "Foo"]} (encode-value (xref :resource :foo))))) 18 | 19 | (testing "pseudo-account value" 20 | (is (= {"Ref" "AWS::AccountId"} (encode-value (pseudo :account-id))))) 21 | 22 | (testing "pseudo-region value" 23 | (is (= {"Ref" "AWS::Region"} (encode-value (pseudo :region))))) 24 | 25 | (testing "pseudo-notification-arns value" 26 | (is (= {"Ref" "AWS::NotificationARNs"} (encode-value (pseudo :notification-arns))))) 27 | 28 | (testing "no-value value" 29 | (is (= {"Ref" "AWS::NoValue"} (encode-value (pseudo :no-value))))) 30 | 31 | (testing "stack-id value" 32 | (is (= {"Ref" "AWS::StackId"} (encode-value (pseudo :stack-id))))) 33 | 34 | (testing "stack-name value" 35 | (is (= {"Ref" "AWS::StackName"} (encode-value (pseudo :stack-name))))) 36 | 37 | (testing "join fn strings value" 38 | (is (= {"Fn::Join" ["." ["foo" "bar"]]} (encode-value (join "." ["foo" "bar"]))))) 39 | 40 | (testing "something" 41 | (is (= {"Fn::If" ["o" "foo" "bar"]} (encode-value (fn-if "o" "foo" "bar"))))) 42 | 43 | (testing "select fn string value" 44 | (is (= {"Fn::Select" ["1" ["foo" "bar"]]} (encode-value (select 1 ["foo" "bar"]))))) 45 | 46 | (testing "select fn select value" 47 | (is (= {"Fn::Select" ["1" ["foo" {"Ref" "Blah"}]]} 48 | (encode-value (select 1 ["foo" (xref :blah)]))))) 49 | 50 | (testing "property value walk" 51 | (is (= {"Fn::Join" ["-" [{"Ref" "Foo"} "bar" {"Ref" "AWS::AccountId"}]]} 52 | (encode-value (join "-" [(xref :foo) "bar" (pseudo :account-id)])))))) 53 | -------------------------------------------------------------------------------- /src/crucible/encoding/main.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.encoding.main 2 | "Supports generating and writing templates to files. Intended for 3 | ad-hoc manual use and build tooling." 4 | (:gen-class) 5 | (:require [crucible.encoding :refer [encode]] 6 | [clojure.string :as string] 7 | [clojure.tools.namespace.repl :as ns-repl] 8 | [clojure.tools.cli :refer [parse-opts]] 9 | [clojure.java.io :as io] 10 | [clojure.spec.alpha :as s] 11 | [clojure.spec.test.alpha :as stest])) 12 | 13 | (defn template-var->write-location [tvar] 14 | (let [template-ns (ns-name (:ns (meta tvar))) 15 | template-name (:name (meta tvar))] 16 | (str (string/replace template-ns #"[.]" "/") 17 | "/" 18 | template-name 19 | ".json"))) 20 | 21 | (defn write-template [output-location template-var] 22 | (let [output-file (str output-location "/" (template-var->write-location template-var)) 23 | template (encode @template-var)] 24 | (io/make-parents output-file) 25 | (spit output-file template) 26 | output-file)) 27 | 28 | (defn find-templates [] 29 | (ns-repl/refresh) 30 | (mapcat #(->> % 31 | ns-publics 32 | seq 33 | (filter (fn [[k v]] (-> v deref meta :crucible.core/template))) 34 | (map (fn [[_ template-var]] template-var))) 35 | (all-ns))) 36 | 37 | (def cli-options [["-o" "--output-directory DIRECTORY" "Output Directory" 38 | :default "target/templates"] 39 | ["-h" "--help"]]) 40 | 41 | (defn exit [status msg] 42 | (println msg) 43 | (System/exit status)) 44 | 45 | (defn -main 46 | "Write the templates defined in the namespaces to the output path." 47 | [& args] 48 | (let [{:keys [options arguments errors summary]} (parse-opts args cli-options)] 49 | (cond 50 | (:help options) (exit 0 summary) 51 | errors (exit 1 errors)) 52 | (doseq [template-var (find-templates)] 53 | (println "Created template:" (write-template (:output-directory options) template-var))))) 54 | -------------------------------------------------------------------------------- /src/crucible/aws/neptune/db_cluster.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.neptune.db-cluster 2 | "AWS::Neptune::DBCluster" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.aws.neptune :as neptune] 5 | [crucible.encoding.keys :refer [->key]] 6 | [crucible.resources :refer [spec-or-ref defresource]])) 7 | 8 | (s/def ::availability-zones (spec-or-ref (s/coll-of (spec-or-ref string?) 9 | :kind vector?))) 10 | 11 | (s/def ::backup-retention-period (spec-or-ref pos-int?)) 12 | 13 | (s/def ::db-cluster-identifier ::neptune/db-cluster-identifier) 14 | 15 | (s/def ::db-cluster-parameter-group-name (spec-or-ref string?)) 16 | (defmethod ->key :db-cluster-parameter-group-name [_] "DBClusterParameterGroupName") 17 | 18 | (s/def ::db-subnet-group-name ::neptune/db-subnet-group-name) 19 | 20 | (s/def ::iam-auth-enabled (spec-or-ref boolean?)) 21 | 22 | (s/def ::kms-key-id (spec-or-ref string?)) 23 | 24 | (s/def ::port (spec-or-ref pos-int?)) 25 | 26 | (s/def ::preferred-backup-window (spec-or-ref string?)) 27 | 28 | (s/def ::preferred-maintenance-window (spec-or-ref string?)) 29 | 30 | (s/def ::snapshot-identifier (spec-or-ref string?)) 31 | 32 | (s/def ::storage-encrypted (spec-or-ref boolean?)) 33 | 34 | (s/def ::vpc-security-group-ids (spec-or-ref (s/coll-of (spec-or-ref string?) 35 | :kind vector?))) 36 | 37 | (s/def ::db-cluster 38 | (s/keys :opt [::availability-zones 39 | ::backup-retention-period 40 | ::db-cluster-identifier 41 | ::db-cluster-parameter-group-name 42 | ::db-subnet-group-name 43 | ::iam-auth-enabled 44 | ::kms-key-id 45 | ::port 46 | ::preferred-backup-window 47 | ::preferred-maintenance-window 48 | ::snapshot-identifier 49 | ::storage-encrypted 50 | :crucible.resources/tags 51 | ::vpc-security-group-ids])) 52 | 53 | (defresource db-cluster "AWS::Neptune::DBCluster" ::db-cluster) 54 | -------------------------------------------------------------------------------- /src/crucible/resources.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.resources 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.policies :as policies] 4 | [expound.alpha :as expound])) 5 | 6 | (s/def ::props-type keyword?) 7 | 8 | (s/def ::resource-property-value any?) 9 | 10 | (defmulti properties-type ::props-type) 11 | 12 | (s/def ::generic (s/map-of keyword? ::resource-property-value)) 13 | 14 | (defmethod properties-type ::generic [_] ::generic) 15 | 16 | (s/def ::properties any?) 17 | 18 | (s/def ::type (s/and string? #(re-matches #"([a-zA-Z0-9]+::)+[a-zA-Z0-9]+" %))) 19 | 20 | (s/def ::resource (s/keys :req [::type ::properties] 21 | :opt [::policies/policies])) 22 | 23 | (defmacro spec-or-ref 24 | "Allows the given spec, keyed as :literal, or a referenced value, keyed as :reference." 25 | [spec] 26 | `(s/or :literal ~spec 27 | :reference :crucible.values/value)) 28 | 29 | (s/def ::key string?) 30 | (s/def ::value (spec-or-ref string?)) 31 | (s/def ::tag (s/keys :req [::key ::value])) 32 | (s/def ::tags (s/* ::tag)) 33 | 34 | (defn- assoc-when [m condition k v] (if condition 35 | (assoc m k v) 36 | m)) 37 | 38 | (def invalid? (complement s/valid?)) 39 | 40 | (s/def ::policy-list (s/* ::policies/policies)) 41 | 42 | (defn resource-factory [resource-type props-spec] 43 | (if-not (s/valid? ::type resource-type) 44 | (throw (ex-info (str "Invalid resource name" (expound/expound-str ::type resource-type)) 45 | (s/explain-data ::type resource-type))) 46 | (fn [& [props & policies]] 47 | [:resource 48 | (cond 49 | (invalid? props-spec props) 50 | (throw (ex-info (str "Invalid resource properties" (expound/expound-str props-spec props)) 51 | (s/explain-data props-spec props))) 52 | :else (-> {::type resource-type 53 | ::properties props} 54 | (merge (into {} (s/conform ::policy-list policies)))))]))) 55 | 56 | (defmacro defresource 57 | "Adds a resource factory function to the namespace, documenting the AWS type" 58 | [sym resource-type props-spec] 59 | `(def ~(vary-meta 60 | sym 61 | assoc :doc (str "CloudFormation Type " resource-type)) 62 | (resource-factory ~resource-type ~props-spec))) 63 | -------------------------------------------------------------------------------- /src/crucible/aws/route53/record_set.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.route53.record-set 2 | "AWS::Route53::RecordSet" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.encoding.keys :refer [->key]] 5 | [crucible.resources :as res :refer [spec-or-ref]])) 6 | 7 | ;; recordset https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html 8 | 9 | ;; alias-target 10 | (s/def ::dns-name (spec-or-ref string?)) 11 | (s/def ::evaluate-target-health (spec-or-ref string?)) 12 | 13 | ;; geo-location 14 | (s/def ::continent-code (spec-or-ref string?)) 15 | (s/def ::country-code (spec-or-ref string?)) 16 | (s/def ::subdivision-code (spec-or-ref string?)) 17 | 18 | (s/def ::name (spec-or-ref string?)) 19 | (s/def ::alias-target (s/keys :req [::dns-name 20 | ::hosted-zone-id] 21 | :opt [::evaluate-target-health])) 22 | (s/def ::comment (spec-or-ref string?)) 23 | (s/def ::failover (spec-or-ref string?)) 24 | (s/def ::geo-location (s/keys :opt [::continent-code 25 | ::country-code 26 | ::subdivision-code])) 27 | (s/def ::health-check-id (spec-or-ref string?)) 28 | (s/def ::hosted-zone-id (spec-or-ref string?)) 29 | (s/def ::hosted-zone-name (spec-or-ref string?)) 30 | (s/def ::region (spec-or-ref string?)) 31 | (s/def ::resource-records (s/coll-of (spec-or-ref string?) :kind vector?)) 32 | (s/def ::set-identifier (spec-or-ref string?)) 33 | (s/def ::ttl (spec-or-ref string?)) 34 | (s/def ::type (spec-or-ref string?)) 35 | (s/def ::weight (spec-or-ref integer?)) 36 | 37 | (s/def ::resource-spec (s/keys :req [::name] 38 | :opt [::alias-target 39 | ::comment 40 | ::failover 41 | ::geo-location 42 | ::health-check-id 43 | ::hosted-zone-id 44 | ::hosted-zone-name 45 | ::region 46 | ::resource-records 47 | ::set-identifier 48 | ::ttl 49 | ::type 50 | ::weight])) 51 | 52 | (defmethod ->key :ttl [_] "TTL") 53 | (defmethod ->key :dns-name [_] "DNSName") 54 | -------------------------------------------------------------------------------- /src/crucible/aws/serverless/function.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function 2 | (:require [crucible.aws.iam :as iam] 3 | [crucible.aws.lambda :as lambda] 4 | [crucible.aws.serverless :as sam] 5 | [crucible.aws.serverless.function.dead-letter-queue :as dlq] 6 | [crucible.aws.serverless.function.event-source :as event-source] 7 | [crucible.resources :refer [spec-or-ref defresource]] 8 | [clojure.spec.alpha :as s])) 9 | 10 | (s/def ::handler (spec-or-ref string?)) 11 | 12 | (s/def ::runtime (spec-or-ref string?)) 13 | 14 | (s/def ::code-uri 15 | (spec-or-ref 16 | (s/or :string string? 17 | :s3-location ::sam/s3-location))) 18 | 19 | (s/def ::inline-code (spec-or-ref string?)) 20 | 21 | (s/def ::function-name (spec-or-ref (s/and string? #(<= (count %) 64)))) 22 | 23 | (s/def ::description (spec-or-ref string?)) 24 | 25 | (s/def ::memory-size (spec-or-ref int?)) 26 | 27 | (s/def ::timeout (spec-or-ref int?)) 28 | 29 | (s/def ::role ::sam/arn) 30 | 31 | (s/def ::policies (s/or :name string? 32 | :policy ::iam/policy-document 33 | :list (s/* (s/or :name string? 34 | :policy ::iam/policy-document)))) 35 | 36 | (s/def ::events (spec-or-ref (s/map-of (s/or :string string? :keyword keyword?) 37 | ::event-source/event-source))) 38 | 39 | (s/def ::vpc-config ::lambda/vpc-config) 40 | 41 | (s/def ::variables ::sam/variables) 42 | 43 | (s/def ::environment (s/keys :req [::variables])) 44 | 45 | (s/def ::tags ::sam/tags) 46 | 47 | (s/def ::reserved-concurrent-executions (spec-or-ref int?)) 48 | 49 | (s/def ::function 50 | (s/keys :opt [::handler ; required but could be in globals 51 | ::runtime ; required but could be in globals 52 | ;; either code-uri or inline-code are required but either could 53 | ;; be defined in globals 54 | ::code-uri 55 | ::inline-code 56 | ::function-name 57 | ::description 58 | ::memory-size 59 | ::timeout 60 | ::role 61 | ::policies 62 | ::environment 63 | ::vpc-config 64 | ::tags 65 | ::dlq/dead-letter-queue 66 | ::reserved-concurrent-executions])) 67 | 68 | (defresource function "AWS::Serverless::Function" ::function) 69 | -------------------------------------------------------------------------------- /test/crucible/aws/sns_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.sns-test 2 | (:require [crucible.core :refer [template xref]] 3 | [crucible.aws.sns :as sns] 4 | [crucible.aws.iam :as iam] 5 | [crucible.encoding :refer [rewrite-element-data]] 6 | [clojure.test :refer :all] 7 | [crucible.encoding :as enc])) 8 | 9 | (deftest sns-topic-test 10 | (testing "encode" 11 | (is (= {"Type" "AWS::SNS::Topic", 12 | "Properties" 13 | {"TopicName" "SampleTopic"}} 14 | (rewrite-element-data 15 | (sns/topic {::sns/topic-name "SampleTopic"}))))) 16 | 17 | (testing "encode" 18 | (is (= {"Type" "AWS::SNS::Topic", 19 | "Properties" 20 | {"TopicName" "SampleTopicWithSubscription" 21 | "Subscription" [{"Protocol" "email" 22 | "Endpoint" "test@test.com"}]}} 23 | (rewrite-element-data 24 | (sns/topic {::sns/topic-name "SampleTopicWithSubscription" 25 | ::sns/subscription [{::sns/protocol "email" 26 | ::sns/endpoint "test@test.com"}]})))))) 27 | 28 | (deftest sns-topic-policy 29 | (testing "encode" 30 | (is (= {"Type" "AWS::SNS::TopicPolicy" 31 | "Properties" 32 | {"Topics" [{"Fn::GetAtt" ["Foo" "Arn"]}] 33 | "PolicyDocument" 34 | {"Statement" 35 | [{"Sid" "AllowS3ToPublish" 36 | "Effect" "Allow" 37 | "Principal" {"Service" "s3.amazonaws.com"} 38 | "Action" "sns:Publish" 39 | "Resource" {"Ref" "Foo"}} 40 | {"Sid" "AllowAnyToSubscribe" 41 | "Effect" "Allow" 42 | "Principal" "*" 43 | "Action" "sns:Subscribe" 44 | "Resource" {"Ref" "Foo"}}]}}} 45 | (rewrite-element-data 46 | (sns/topic-policy {::sns/topics [(xref :foo :arn)] 47 | ::iam/policy-document 48 | {::iam/statement 49 | [{::iam/sid "AllowS3ToPublish" 50 | ::iam/effect "Allow" 51 | ::iam/principal {"Service" "s3.amazonaws.com"} 52 | ::iam/action "sns:Publish" 53 | ::iam/resource (xref :foo)} 54 | {::iam/sid "AllowAnyToSubscribe" 55 | ::iam/effect "Allow" 56 | ::iam/principal "*" 57 | ::iam/action "sns:Subscribe" 58 | ::iam/resource (xref :foo)}]}})))))) 59 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject crucible (or (System/getenv "PROJECT_VERSION") "0.0.0-SNAPSHOT") 2 | :description "AWS Cloudformation templates in Clojure" 3 | :url "http://github.com/brabster/crucible" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[camel-snake-kebab "0.4.1"] 7 | [cheshire "5.10.0"] 8 | [org.clojure/tools.namespace "1.0.0"] 9 | [org.clojure/tools.cli "1.0.194"] 10 | [expound "0.8.4"]] 11 | :dependency-check {:throw true} 12 | :exclusions [org.clojure/clojure] 13 | :codox {:output-path "target/docs" 14 | :source-uri "https://github.com/brabster/crucible/blob/{version}/{filepath}#L{line}"} 15 | :plugins [[org.clojure/tools.cli "0.4.2" :exclusions [org.clojure/clojure]] 16 | [lein-ancient "0.6.15"] 17 | [com.livingsocial/lein-dependency-check "1.1.4"] 18 | [lein-kibit "0.1.8" :exclusions [org.clojure/clojure 19 | org.clojure/tools.cli]] 20 | [jonase/eastwood "0.3.7"] 21 | [lein-bikeshed "0.5.2"] 22 | [lein-cloverage "1.1.2"] 23 | [lein-codox "0.10.7"]] 24 | :repositories [["snapshots" {:url "https://clojars.org/repo" 25 | :username :env/clojars_username 26 | :password :env/clojars_password}] 27 | ["releases" {:url "https://clojars.org/repo" 28 | :username :env/clojars_username 29 | :password :env/clojars_password 30 | :sign-releases false}]] 31 | :target-path "target/%s" 32 | :main crucible.encoding.main 33 | :aliases {"qa" ["do" 34 | ["clean"] 35 | ["check"] 36 | ["eastwood"] 37 | ["bikeshed" "-m" "180"] 38 | ["cloverage"]] 39 | "third-party-check" ["do" 40 | ["ancient"] 41 | ["dependency-check"]]} 42 | :eastwood {:include-linters [:keyword-typos 43 | :non-clojure-file 44 | :unused-fn-args 45 | :unused-locals 46 | :unused-namespaces 47 | :unused-private-vars 48 | :unused-private-vars] 49 | :exclude-linters [:suspicious-expression]} 50 | :profiles {:uberjar {:aot :all} 51 | :provided {:dependencies [[org.clojure/clojure "1.10.1"]]} 52 | :dev {:resource-paths ["test-resources"] 53 | :dependencies [[org.clojure/test.check "1.0.0"]]}}) 54 | -------------------------------------------------------------------------------- /src/crucible/encoding.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.encoding 2 | (:require [clojure.walk :as walk] 3 | [cheshire.core :as json] 4 | [crucible.values :as v] 5 | [crucible.resources :as r] 6 | [crucible.encoding.keys :refer [->key]])) 7 | 8 | (defmethod ->key :aws-template-format-version [_] 9 | "AWSTemplateFormatVersion") 10 | 11 | (defmulti rewrite-element-data 12 | "Convert the crucible representation of an element into a form ready 13 | for JSON encoding" 14 | (fn [[type _]] type)) 15 | 16 | (defn- unqualify-keyword 17 | "Remove namespace qualification from a keyword for JSON encoding" 18 | [kw] 19 | (-> kw name keyword)) 20 | 21 | (defn convert-key 22 | "Prepare key for encoding as JSON" 23 | [k] 24 | (-> k unqualify-keyword ->key)) 25 | 26 | (defmethod rewrite-element-data :default 27 | [[_ element]] 28 | (walk/prewalk 29 | (fn [x] 30 | (cond 31 | (::v/type x) (v/encode-value x) 32 | (keyword? x) (convert-key x) 33 | :else x)) 34 | element)) 35 | 36 | (defmethod rewrite-element-data :resource 37 | [[_ element]] 38 | (walk/prewalk 39 | (fn [x] 40 | (cond 41 | (::r/policies x) (-> x 42 | (merge (::r/policies x)) 43 | (dissoc ::r/policies)) 44 | (::v/type x) (v/encode-value x) 45 | (keyword? x) (convert-key x) 46 | :else x)) 47 | element)) 48 | 49 | (defmethod rewrite-element-data :mapping 50 | [[_ element]] 51 | element) 52 | 53 | (defn- rewrite-element [[key {:keys [type specification]}]] 54 | [(->key key) [type (rewrite-element-data [type specification])]]) 55 | 56 | (defn- element-type->cf-section [element-type] 57 | (-> element-type 58 | name 59 | (str "s") 60 | keyword 61 | ->key)) 62 | 63 | (defn- assemble-template [m [k v]] 64 | (let [cf-section (element-type->cf-section (first v)) 65 | element-data (second v)] 66 | (assoc-in m [cf-section k] element-data))) 67 | 68 | (defn- elements->template 69 | "Prepare the elements map for JSON encoding" 70 | [elements-map empty-template] 71 | (->> elements-map 72 | seq 73 | (map rewrite-element) 74 | (reduce assemble-template empty-template))) 75 | 76 | (defn build 77 | "Create a CloudFormation-compatible data structure ready for JSON encoding from the template" 78 | [template] 79 | (-> template 80 | :elements 81 | (elements->template {(->key :aws-template-format-version) "2010-09-09" 82 | (->key :description) (or (:description template) 83 | "No description provided")}))) 84 | 85 | (defn encode 86 | "Convert the template data structure into a JSON-encoded string" 87 | [template] 88 | (json/encode (build template))) 89 | -------------------------------------------------------------------------------- /src/crucible/aws/elbv2/target_group.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.elbv2.target-group 2 | "AWS::ElasticLoadBalancingV2::TargetGroup" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res])) 5 | 6 | ;; http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html 7 | 8 | 9 | (s/def ::key (spec-or-ref string?)) 10 | (s/def ::value (spec-or-ref string?)) 11 | (s/def ::availability-zone (spec-or-ref string?)) 12 | (s/def ::id (spec-or-ref string?)) 13 | (s/def ::port (spec-or-ref integer?)) 14 | (s/def ::health-check-interval-seconds (spec-or-ref integer?)) 15 | (s/def ::health-check-path (spec-or-ref string?)) 16 | (s/def ::health-check-port (spec-or-ref string?)) ;; eg "traffic-port" 17 | (s/def ::health-check-protocol (spec-or-ref #{"HTTPS" "HTTP" "TCP"})) 18 | (s/def ::health-check-timeout-seconds (spec-or-ref #(s/int-in-range? 2 60 %))) 19 | (s/def ::healthy-threshold-count (spec-or-ref #(s/int-in-range? 2 10 %))) 20 | (s/def ::http-code (spec-or-ref integer?)) 21 | (s/def ::matcher (spec-or-ref (s/keys :opt [::http-code]))) 22 | (s/def ::name (spec-or-ref string?)) 23 | (s/def ::port (spec-or-ref #(s/int-in-range? 1 65535 %))) 24 | (s/def ::protocol (spec-or-ref (spec-or-ref #{"HTTPS" "HTTP" "TCP"}))) 25 | (s/def ::target-group-attribute (s/keys :opt [::key ::value])) 26 | (s/def ::target-group-attributes (s/* ::target-group-attribute)) 27 | 28 | (s/def ::target-description (s/keys :req [::id] 29 | :opt [::availability-zone 30 | ::port])) 31 | (s/def ::targets (s/* ::target-description)) 32 | (s/def ::target-type (spec-or-ref #{"instance" "ip"})) 33 | (s/def ::unhealthy-threshold-count (spec-or-ref integer?)) 34 | (s/def ::vpc-id (spec-or-ref string?)) 35 | 36 | (s/def ::resource-spec (s/keys :req [::vpc-id 37 | ::port 38 | ::protocol] 39 | :opt [::health-check-interval-seconds 40 | ::health-check-path 41 | ::health-check-port 42 | ::health-check-protocol 43 | ::health-check-timeout-seconds 44 | ::healthy-threshold-count 45 | ::matcher 46 | ::name 47 | ::res/tags 48 | ::target-group-attributes 49 | ::targets 50 | ::target-type 51 | ::unhealthy-threshold-count])) 52 | -------------------------------------------------------------------------------- /test/crucible/aws/serverless/function_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.serverless.function-test 2 | (:require [crucible.aws.serverless :as sam] 3 | [crucible.aws.serverless.function :as f] 4 | [crucible.aws.serverless.function.event-source :as es] 5 | [crucible.aws.serverless.function.event-source.kinesis :as k] 6 | [crucible.core :refer [xref]] 7 | [crucible.resources :as res] 8 | [clojure.test :refer :all] 9 | [crucible.aws.iam :as iam])) 10 | 11 | (deftest function-test 12 | (testing "encode" 13 | (is (= {"Type" "AWS::Serverless::Function" 14 | "Properties" 15 | {"Handler" "index.js" 16 | "Runtime" "nodejs6.10" 17 | "CodeUri" "s3://my-code-bucket/my-function.zip" 18 | "MemorySize" 1024 19 | "Timeout" 15 20 | "Policies" ["AWSLambdaExecute" 21 | {"Version" "2012-10-17" 22 | "Statement" [{"Effect" "Allow" 23 | "Action" ["s3:GetObject" 24 | "s3:GetObjectACL"] 25 | "Resource" "arn:aws:s3:::my-bucket/*"}]}] 26 | "Environment" {"Variables" {"TableName" "my-table"}} 27 | "Events" {"PhotoUpload" {"Type" "S3" 28 | "Properties" {"Bucket" "my-photo-bucket"}}} 29 | "Tags" {"AppNameTag" "ThumbnailApp" 30 | "DepartmentNameTag" "ThumbnailDepartment"} 31 | "Layers" "arn:aws:lambda:${AWS:Region}:123456789012:layer:MyLayer:1"}} 32 | (crucible.encoding/rewrite-element-data 33 | (f/function 34 | {::f/handler "index.js" 35 | ::f/runtime "nodejs6.10" 36 | ::f/code-uri "s3://my-code-bucket/my-function.zip" 37 | ::f/memory-size 1024 38 | ::f/timeout 15 39 | ::f/policies ["AWSLambdaExecute" 40 | {::iam/version "2012-10-17" 41 | ::iam/statement [{::iam/effect "Allow" 42 | ::iam/action ["s3:GetObject" 43 | "s3:GetObjectACL"] 44 | ::iam/resource "arn:aws:s3:::my-bucket/*"}]}] 45 | ::f/environment {::f/variables {:TABLE_NAME "my-table"}} 46 | ::f/events {:photo-upload {::es/type "S3" 47 | ::properties {::bucket "my-photo-bucket"}}} 48 | ::f/tags {:app-name-tag "ThumbnailApp" 49 | :department-name-tag "ThumbnailDepartment"} 50 | ::f/layers "arn:aws:lambda:${AWS:Region}:123456789012:layer:MyLayer:1"})))))) 51 | -------------------------------------------------------------------------------- /test/crucible/aws/lambda_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.lambda-test 2 | (:require [clojure.test :refer :all] 3 | [crucible.aws.lambda :as l] 4 | [crucible.resources :as res] 5 | [cheshire.core :as json] 6 | [clojure.spec.alpha :as s] 7 | [crucible.core :refer [xref account-id parameter]] 8 | [crucible.assertion :refer [resource=]])) 9 | 10 | (deftest function-test 11 | (testing "encode" 12 | (is (= {"Type" "AWS::Lambda::Function" 13 | "Properties" {"FunctionName" {"Ref" "Foo"} 14 | "Timeout" 300 15 | "Handler" {"Ref" "Foo"} 16 | "Runtime" "java8" 17 | "MemorySize" 1024 18 | "ReservedConcurrentExecutions" 100 19 | "Role" {"Ref" "Foo"} 20 | "Description" {"Ref" "Foo"} 21 | "Environment" {"Variables" {"Foo" "Bar" 22 | "Bar" "Baz"}} 23 | "VpcConfig" {"SecurityGroupIds" [{"Ref" "Foo"}] 24 | "SubnetIds" {"Ref" "Foo"}} 25 | "Code" {"S3Bucket" {"Ref" "Foo"}, "S3Key" {"Ref" "Foo"}}}} 26 | (crucible.encoding/rewrite-element-data 27 | (l/function {::l/handler (xref :foo) 28 | ::l/function-name (xref :foo) 29 | ::l/description (xref :foo) 30 | ::l/memory-size 1024 31 | ::l/timeout 300 32 | ::l/reserved-concurrent-executions 100 33 | ::l/runtime "java8" 34 | ::l/role (xref :foo) 35 | ::l/code {::l/s3-bucket (xref :foo) 36 | ::l/s3-key (xref :foo)} 37 | ::l/environment {::l/variables {:foo "Bar" 38 | "Bar" "Baz"}} 39 | ::l/vpc-config {::l/security-group-ids [(xref :foo)] 40 | ::l/subnet-ids (xref :foo)}})))))) 41 | 42 | (deftest permission-test 43 | (testing "encode" 44 | (is (resource= {"Type" "AWS::Lambda::Permission" 45 | "Properties" {"FunctionName" { "Fn::GetAtt" ["MyLambdaFunction" "Arn"] } 46 | "Action" "lambda:InvokeFunction" 47 | "Principal" "s3.amazonaws.com" 48 | "SourceAccount" { "Ref" "AWS::AccountId" }}} 49 | (l/permission {::l/action "lambda:InvokeFunction" 50 | ::l/function-name (xref :my-lambda-function :arn) 51 | ::l/principal "s3.amazonaws.com" 52 | ::l/source-account account-id}))))) 53 | -------------------------------------------------------------------------------- /test/crucible/parameters_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.parameters-test 2 | (:require [crucible.core :refer [parameter]] 3 | [crucible.parameters :as p] 4 | [crucible.encoding :as enc] 5 | [clojure.test :refer :all] 6 | [clojure.spec.alpha :as s])) 7 | 8 | (deftest parameters-test 9 | (testing "comma delimited list" 10 | (is (= {"Type" "CommaDelimitedList"} 11 | (enc/rewrite-element-data (parameter ::p/type ::p/comma-delimited-list))))) 12 | 13 | (testing "string default" 14 | (is (= {"Type" "String"} 15 | (enc/rewrite-element-data (parameter))))) 16 | 17 | (testing "number" 18 | (is (= {"Type" "Number"} 19 | (enc/rewrite-element-data (parameter ::p/type ::p/number))))) 20 | 21 | (testing "list of numbers" 22 | (is (= {"Type" "List"} 23 | (enc/rewrite-element-data (parameter ::p/type ::p/list-number)))))) 24 | 25 | (deftest parameter-attributes-test 26 | (testing "default value" 27 | (is (= {"Type" "String" "Default" "foo"} 28 | (enc/rewrite-element-data (parameter ::p/default "foo"))))) 29 | 30 | (testing "description" 31 | (is (= {"Type" "String" "Description" "foo"} 32 | (enc/rewrite-element-data (parameter ::p/description "foo"))))) 33 | 34 | (testing "constraint description" 35 | (is (= {"Type" "String" "ConstraintDescription" "foo"} 36 | (enc/rewrite-element-data (parameter ::p/constraint-description "foo"))))) 37 | 38 | (testing "allowed values" 39 | (is (= {"Type" "String" "AllowedValues" ["foo" "bar"]} 40 | (enc/rewrite-element-data (parameter ::p/allowed-values ["foo" "bar"]))))) 41 | 42 | (testing "allowed pattern" 43 | (is (= {"Type" "String" "AllowedPattern" "[a-z]+"} 44 | (enc/rewrite-element-data (parameter ::p/allowed-pattern "[a-z]+"))))) 45 | 46 | (testing "max value" 47 | (is (= {"Type" "String" "MaxValue" 1} 48 | (enc/rewrite-element-data (parameter ::p/max-value 1))))) 49 | 50 | (testing "min value" 51 | (is (= {"Type" "String" "MinValue" 1} 52 | (enc/rewrite-element-data (parameter ::p/min-value 1))))) 53 | 54 | (testing "max length" 55 | (is (= {"Type" "String" "MaxLength" 1} 56 | (enc/rewrite-element-data (parameter ::p/max-length 1))))) 57 | 58 | (testing "min length" 59 | (is (= {"Type" "String" "MinLength" 1} 60 | (enc/rewrite-element-data (parameter ::p/min-length 1))))) 61 | 62 | (testing "no echo" 63 | (is (= {"Type" "String" "NoEcho" true} 64 | (enc/rewrite-element-data (parameter ::p/no-echo true)))))) 65 | 66 | (deftest aws-specific-parameters-test 67 | (is (= {"Type" "AWS::EC2::AvailabilityZone::Name"} 68 | (enc/rewrite-element-data (parameter ::p/type ::p/aws-ec2-az-name)))) 69 | 70 | (is (= {"Type" "AWS::EC2::Image::Id"} 71 | (enc/rewrite-element-data (parameter ::p/type ::p/aws-ec2-image-id)))) 72 | 73 | (is (= {"Type" "AWS::EC2::Instance::Id"} 74 | (enc/rewrite-element-data (parameter ::p/type ::p/aws-ec2-instance-id))))) 75 | -------------------------------------------------------------------------------- /src/crucible/policies.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.policies 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.encoding.keys :refer [->key]])) 4 | 5 | (s/def ::min-successful-instances-percent int?) 6 | (s/def ::auto-scaling-creation-policy (s/keys :opt [::min-successful-instances-percent])) 7 | 8 | (s/def ::count int?) 9 | (s/def ::timeout string?) 10 | (s/def ::resource-signal (s/keys :opt [::count 11 | ::timeout])) 12 | 13 | (s/def ::creation-policy (s/keys :req 14 | [(or ::auto-scaling-creation-policy 15 | ::resource-signal)])) 16 | 17 | (s/def ::metadata (s/keys)) 18 | 19 | (s/def ::will-replace boolean?) 20 | (s/def ::auto-scaling-replacing-update (s/keys :opt [::will-replace])) 21 | 22 | (s/def ::max-batch-size int?) 23 | (s/def ::min-instances-in-service int?) 24 | (s/def ::pause-time string?) 25 | (s/def ::suspend-processes (s/* string?)) 26 | (s/def ::wait-on-resource-signals boolean?) 27 | 28 | (s/def ::auto-scaling-rolling-update (s/keys :opt [::max-batch-size 29 | ::min-instances-in-service 30 | ::min-successful-instances-percent 31 | ::pause-time 32 | ::suspend-processes 33 | ::wait-on-resource-signals])) 34 | 35 | (s/def ::ignore-unmodified-groups-size-properties boolean?) 36 | (s/def ::auto-scaling-scheduled-action (s/keys :opt [::ignore-unmodified-groups-size-properties])) 37 | 38 | (s/def ::update-policy (s/keys :req 39 | [(or ::auto-scaling-replacing-update 40 | ::auto-scaling-rolling-update 41 | ::auto-scaling-scheduled-action)])) 42 | 43 | (s/def ::deletion-policy #{::retain ::delete ::snapshot}) 44 | 45 | (s/def ::depends-on keyword?) 46 | 47 | (s/def ::condition keyword?) 48 | 49 | (s/def ::policy (s/or 50 | :deletion-policy ::deletion-policy 51 | :depends-on ::depends-on 52 | :creation-policy ::creation-policy 53 | :update-policy ::update-policy 54 | :metadata ::metadata 55 | :condition ::condition)) 56 | 57 | (s/def ::policies (s/keys :opt [::deletion-policy 58 | ::depends-on 59 | ::creation-policy 60 | ::update-policy 61 | ::metadata 62 | ::condition])) 63 | 64 | 65 | (defn deletion [policy] 66 | {::deletion-policy policy}) 67 | 68 | (defn depends-on [kw] 69 | {::depends-on kw}) 70 | 71 | (defn creation-policy [policy] 72 | {::creation-policy policy}) 73 | 74 | (defn update-policy [policy] 75 | {::update-policy policy}) 76 | 77 | (defn metadata [policy] 78 | {::metadata policy}) 79 | 80 | (defn condition [kw] 81 | {::condition kw}) -------------------------------------------------------------------------------- /src/crucible/aws/firehose.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.firehose 2 | "Resources in AWS::KinesisFirehose::DeliveryStream" 3 | (:require [crucible.resources :refer [spec-or-ref defresource] :as res] 4 | [clojure.spec.alpha :as s] 5 | [crucible.encoding.keys :refer [->key]])) 6 | 7 | (defmethod ->key :size-in-mbs [_] 8 | "SizeInMBs") 9 | 10 | (defmethod ->key :kms-encryption-config [_] 11 | "KMSEncryptionConfig") 12 | 13 | (defmethod ->key :aws-kms-key-arn [_] 14 | "AWSKMSKeyARN") 15 | 16 | (defmethod ->key :role-arn [_] 17 | "RoleARN") 18 | 19 | (defmethod ->key :bucket-arn [_] 20 | "BucketARN") 21 | 22 | (defmethod ->key :kinesis-stream-arn [_] 23 | "KinesisStreamARN") 24 | 25 | (s/def ::arn (spec-or-ref string?)) 26 | 27 | (s/def ::delivery-stream-name (spec-or-ref string?)) 28 | 29 | (s/def ::delivery-stream-type (spec-or-ref #{"DirectPut" "KinesisStreamAsSource"})) 30 | 31 | (s/def ::bucket-arn ::arn) 32 | 33 | (s/def ::interval-in-seconds (spec-or-ref pos-int?)) 34 | 35 | (s/def ::size-in-mbs (spec-or-ref pos-int?)) 36 | 37 | (s/def ::buffering-hints (s/keys :req [::interval-in-seconds 38 | ::size-in-mbs])) 39 | 40 | (s/def ::compression-format (spec-or-ref #{"UNCOMPRESSED" "GZIP" "ZIP" "Snappy"})) 41 | 42 | (s/def ::prefix (spec-or-ref string?)) 43 | 44 | (s/def ::role-arn ::arn) 45 | 46 | (s/def ::enabled (spec-or-ref boolean?)) 47 | 48 | (s/def ::log-group-name (spec-or-ref string?)) 49 | 50 | (s/def ::log-stream-name (spec-or-ref string?)) 51 | 52 | (s/def ::cloud-watch-logging-options (s/keys :opt [::enabled 53 | ::log-group-name 54 | ::log-stream-name])) 55 | 56 | (s/def ::aws-kms-key-arn ::arn) 57 | 58 | (s/def ::kms-encryption-config (s/keys :req [::aws-kms-key-arn])) 59 | 60 | (s/def ::encryption-configuration (s/keys :opt [::kms-encryption-config 61 | ::no-encryption-config])) 62 | 63 | (s/def ::s3-destination-configuration (s/keys :req [::bucket-arn 64 | ::buffering-hints 65 | ::compression-format 66 | ::prefix 67 | ::role-arn] 68 | :opt [::cloud-watch-logging-options 69 | ::encryption-configuration])) 70 | 71 | (s/def ::kinesis-stream-arn ::arn) 72 | 73 | (s/def ::kinesis-stream-source-configuration (s/keys :req [::kinesis-stream-arn 74 | ::role-arn])) 75 | 76 | (s/def ::firehose (s/keys :opt [::delivery-stream-name 77 | ::delivery-stream-type 78 | ::s3-destination-configuration 79 | ::kinesis-stream-source-configuration])) 80 | 81 | (defresource firehose "AWS::KinesisFirehose::DeliveryStream" ::firehose) 82 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at paul.brabban@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /test/crucible/examples_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.examples-test 2 | (:require [clojure.test :refer :all] 3 | [crucible 4 | [core :refer [template parameter resource output xref encode join stack-name]] 5 | [policies :as pol] 6 | [parameters :as param]] 7 | [crucible.resources :as res] 8 | [crucible.aws.ec2 :as ec2] 9 | [cheshire.core :as json])) 10 | 11 | (def simple (template "A simple sample template" 12 | :my-vpc-cidr (parameter) 13 | :my-vpc (ec2/vpc {::ec2/cidr-block (xref :my-vpc-cidr)}) 14 | :vpc (output (join "/" ["foo" (xref :my-vpc)])))) 15 | 16 | (deftest example-simple 17 | (testing "Matches documented output" 18 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 19 | "Description" "A simple sample template" 20 | "Parameters" {"MyVpcCidr" {"Type" "String"}} 21 | "Resources" {"MyVpc" 22 | {"Type" "AWS::EC2::VPC" 23 | "Properties" {"CidrBlock" {"Ref" "MyVpcCidr"}}}} 24 | "Outputs" {"Vpc" {"Value" {"Fn::Join" ["/" ["foo" {"Ref" "MyVpc"}]]}}}} 25 | (json/decode (encode simple)))))) 26 | 27 | (def more-complex (template "A more complex sample template" 28 | :my-vpc-cidr (parameter ::param/type ::param/string 29 | ::param/allowed-values ["10.0.0.0/24" 30 | "10.0.0.0/16"]) 31 | :my-vpc (ec2/vpc {::ec2/cidr-block (xref :my-vpc-cidr) 32 | ::res/tags [{::res/key "Xref" 33 | ::res/value (xref :my-vpc-cidr)} 34 | {::res/key "String" 35 | ::res/value "Hello"} 36 | {::res/key "StackName" 37 | ::res/value stack-name}]} 38 | (pol/deletion ::pol/retain) 39 | (pol/depends-on :my-vpc-cidr)) 40 | :vpc (output (join "/" ["foo" (xref :my-vpc)])))) 41 | 42 | (deftest example-more-complex 43 | (testing "Matches documented output" 44 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 45 | "Description" "A more complex sample template" 46 | "Outputs" 47 | {"Vpc" {"Value" {"Fn::Join" ["/" ["foo" {"Ref" "MyVpc"}]]}}} 48 | "Parameters" 49 | {"MyVpcCidr" 50 | {"Type" "String", "AllowedValues" ["10.0.0.0/24" "10.0.0.0/16"]}} 51 | "Resources" 52 | {"MyVpc" 53 | {"Type" "AWS::EC2::VPC" 54 | "Properties" 55 | {"CidrBlock" {"Ref" "MyVpcCidr"} 56 | "Tags" 57 | [{"Key" "Xref", "Value" {"Ref" "MyVpcCidr"}} 58 | {"Key" "String", "Value" "Hello"} 59 | {"Key" "StackName", "Value" {"Ref" "AWS::StackName"}}]} 60 | "DeletionPolicy" "Retain" 61 | "DependsOn" "MyVpcCidr"}}} 62 | (json/decode (encode more-complex)))))) -------------------------------------------------------------------------------- /src/crucible/aws/dynamodb.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.dynamodb 2 | "Resources in AWS::DynamoDB::*" 3 | (:require [crucible.encoding.keys :refer [->key]] 4 | [crucible.resources :refer [spec-or-ref defresource]] 5 | [clojure.spec.alpha :as s])) 6 | 7 | (s/def ::table-name (spec-or-ref string?)) 8 | 9 | (s/def ::attribute-name (spec-or-ref string?)) 10 | 11 | (s/def ::attribute-type (spec-or-ref #{"S" "N" "B"})) 12 | 13 | (s/def ::attribute-definition (s/keys :req [::attribute-name 14 | ::attribute-type])) 15 | 16 | (s/def ::attribute-definitions (s/* ::attribute-definition)) 17 | 18 | (s/def ::index-name (spec-or-ref string?)) 19 | 20 | (s/def ::key-type (spec-or-ref #{"HASH" "RANGE"})) 21 | 22 | (s/def ::hash-key (s/keys :req [::attribute-name 23 | ::key-type])) 24 | 25 | (s/def ::range-key (s/keys :req [::attribute-name 26 | ::key-type])) 27 | 28 | (s/def ::key-schema (s/cat :hash ::hash-key 29 | :range (s/? ::range-key))) 30 | 31 | (s/def ::non-key-attributes (s/* (spec-or-ref string?))) 32 | 33 | (s/def ::projection-type (spec-or-ref #{"KEYS_ONLY" "INCLUDE" "ALL"})) 34 | 35 | (s/def ::projection (s/keys :opt [::non-key-attributes 36 | ::projection-type])) 37 | 38 | (s/def ::capacity-units (spec-or-ref (s/or :string string? 39 | :integer int?))) 40 | 41 | (s/def ::read-capacity-units ::capacity-units) 42 | 43 | (s/def ::write-capacity-units ::capacity-units) 44 | 45 | (s/def ::provisioned-throughput (s/keys :req [::read-capacity-units 46 | ::write-capacity-units])) 47 | 48 | (s/def ::global-secondary-index (s/keys :req [::index-name 49 | ::key-schema 50 | ::projection 51 | ::provisioned-throughput])) 52 | 53 | (s/def ::global-secondary-indexes (s/* ::global-secondary-index)) 54 | 55 | (s/def ::local-secondary-index (s/keys :req [::index-name 56 | ::key-schema 57 | ::projection])) 58 | 59 | (s/def ::local-secondary-indexes (s/* ::local-secondary-index)) 60 | 61 | (s/def ::stream-view-type (spec-or-ref #{"KEYS_ONLY" 62 | "NEW_IMAGE" 63 | "OLD_IMAGE" 64 | "NEW_AND_OLD_IMAGES"})) 65 | 66 | (s/def ::stream-specification (s/keys :req [::stream-view-type])) 67 | 68 | (s/def ::sse-enabled (spec-or-ref boolean?)) 69 | 70 | (defmethod ->key ::sse-enabled [_] "SSEEnabled") 71 | 72 | (s/def ::sse-specification 73 | (s/keys :req [::sse-enabled])) 74 | 75 | (defmethod ->key ::sse-specification [_] "SSESpecification") 76 | 77 | (s/def ::table (s/keys :req [::attribute-definitions 78 | ::key-schema 79 | ::provisioned-throughput] 80 | :opt [::table-name 81 | ::stream-specification 82 | ::global-secondary-indexes 83 | ::local-secondary-indexes 84 | ::sse-specification])) 85 | 86 | (defresource table "AWS::DynamoDB::Table" ::table) 87 | -------------------------------------------------------------------------------- /src/crucible/aws/rds/db_instance.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.rds.db-instance 2 | "Resources in AWS::RDS::DBInstance" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res] 5 | [crucible.encoding.keys :refer [->key]])) 6 | 7 | (defmethod ->key :size-in-mbs [_] "SizeInMBs") 8 | (defmethod ->key :db-cluster-identifier [_] "DBClusterIdentifier") 9 | (defmethod ->key :db-instance-class [_] "DBInstanceClass") 10 | (defmethod ->key :db-instance-identifier [_] "DBInstanceIdentifier") 11 | (defmethod ->key :db-name [_] "DBName") 12 | (defmethod ->key :db-parameter-group-name [_] "DBParameterGroupName") 13 | (defmethod ->key :db-security-groups [_] "DBSecurityGroups") 14 | (defmethod ->key :db-snapshot-identifier [_] "DBSnapshotIdentifier") 15 | (defmethod ->key :db-subnet-group-name [_] "DBSubnetGroupName") 16 | (defmethod ->key :domain-iam-role-name [_] "DomainIAMRoleName") 17 | (defmethod ->key :multi-az [_] "MultiAZ") 18 | (defmethod ->key :source-db-instance-identifier [_] "SourceDBInstanceIdentifier") 19 | (defmethod ->key :vpc-security-groups [_] "VPCSecurityGroups") 20 | 21 | (s/def ::allocated-storage (spec-or-ref string?)) 22 | 23 | (s/def ::allow-major-version-upgrade (spec-or-ref string?)) 24 | 25 | (s/def ::auto-minor-version-upgrade (spec-or-ref string?)) 26 | 27 | (s/def ::availability-zone (spec-or-ref string?)) 28 | 29 | (s/def ::backup-retention-period (spec-or-ref string?)) 30 | 31 | (s/def ::character-set-name (spec-or-ref string?)) 32 | 33 | (s/def ::copy-tags-to-snapshot (spec-or-ref boolean?)) 34 | 35 | (s/def ::db-cluster-identifier (spec-or-ref string?)) 36 | 37 | (s/def ::db-instance-class (spec-or-ref string?)) 38 | 39 | (s/def ::db-instance-identifier (spec-or-ref string?)) 40 | 41 | (s/def ::db-name (spec-or-ref string?)) 42 | 43 | (s/def ::db-parameter-group-name (spec-or-ref string?)) 44 | 45 | (s/def ::db-security-groups (s/* (spec-or-ref string?))) 46 | 47 | (s/def ::db-snapshot-identifier (spec-or-ref string?)) 48 | 49 | (s/def ::db-subnet-group-name (spec-or-ref string?)) 50 | 51 | (s/def ::domain (spec-or-ref string?)) 52 | 53 | (s/def ::domain-iam-role-name (spec-or-ref string?)) 54 | 55 | (s/def ::engine (spec-or-ref string?)) 56 | 57 | (s/def ::engine-version (spec-or-ref string?)) 58 | 59 | (s/def ::iops (spec-or-ref number?)) 60 | 61 | (s/def ::kms-key-id (spec-or-ref string?)) 62 | 63 | (s/def ::license-model (spec-or-ref string?)) 64 | 65 | (s/def ::master-username (spec-or-ref string?)) 66 | 67 | (s/def ::master-username-password (spec-or-ref string?)) 68 | 69 | (s/def ::monitoring-interval (spec-or-ref string?)) 70 | 71 | (s/def ::monitoring-role-arn (spec-or-ref string?)) 72 | 73 | (s/def ::multi-az (spec-or-ref boolean?)) 74 | 75 | (s/def ::option-group-name (spec-or-ref string?)) 76 | 77 | (s/def ::port (spec-or-ref string?)) 78 | 79 | (s/def ::preferred-backup-window (spec-or-ref string?)) 80 | 81 | (s/def ::preferred-maintenance-window (spec-or-ref string?)) 82 | 83 | (s/def ::publicly-accessible (spec-or-ref boolean?)) 84 | 85 | (s/def ::source-db-instance-identifier (spec-or-ref string?)) 86 | 87 | (s/def ::storage-encrypted (spec-or-ref boolean?)) 88 | 89 | (s/def ::storage-type (spec-or-ref string?)) 90 | 91 | (s/def ::tags (s/coll-of (spec-or-ref string?) :kind vector?)) 92 | 93 | (s/def ::timezone (spec-or-ref string?)) 94 | 95 | (s/def ::vpc-security-groups (s/coll-of (spec-or-ref string?) :kind vector?)) 96 | -------------------------------------------------------------------------------- /test/crucible/aws/firehose_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.firehose-test 2 | (:require [crucible.core :refer [parameter xref]] 3 | [crucible.assertion :refer [resource=]] 4 | [crucible.aws.firehose :as fh] 5 | [cheshire.core :as json] 6 | [clojure.test :refer :all])) 7 | 8 | (deftest firehose-test 9 | (testing "encode with delivery stream name" 10 | (is (resource= {"Type" "AWS::KinesisFirehose::DeliveryStream", 11 | "Properties" {"DeliveryStreamName" {"Ref" "Foo"}, 12 | "S3DestinationConfiguration" 13 | {"BucketARN" {"Ref" "Foo"}, 14 | "BufferingHints" {"IntervalInSeconds" 9, 15 | "SizeInMBs" {"Ref" "Foo"}}, 16 | "CompressionFormat" "UNCOMPRESSED", 17 | "Prefix" {"Ref" "Foo"}, 18 | "RoleARN" {"Ref" "Foo"}}}} 19 | (fh/firehose 20 | {::fh/delivery-stream-name (xref :foo) 21 | ::fh/s3-destination-configuration 22 | {::fh/bucket-arn (xref :foo) 23 | ::fh/buffering-hints {::fh/interval-in-seconds 9 24 | ::fh/size-in-mbs (xref :foo)} 25 | ::fh/compression-format "UNCOMPRESSED" 26 | ::fh/prefix (xref :foo) 27 | ::fh/role-arn (xref :foo)}})))) 28 | 29 | (testing "encode without delivery stream name" 30 | (is (resource= {"Type" "AWS::KinesisFirehose::DeliveryStream", 31 | "Properties" {"DeliveryStreamName" {"Ref" "Foo"}, 32 | "S3DestinationConfiguration" 33 | {"BucketARN" {"Ref" "Foo"}, 34 | "BufferingHints" {"IntervalInSeconds" 9, 35 | "SizeInMBs" {"Ref" "Foo"}}, 36 | "CompressionFormat" "UNCOMPRESSED", 37 | "Prefix" {"Ref" "Foo"}, 38 | "RoleARN" {"Ref" "Foo"}}}} 39 | (fh/firehose 40 | {::fh/delivery-stream-name (xref :foo) 41 | ::fh/s3-destination-configuration 42 | {::fh/bucket-arn (xref :foo) 43 | ::fh/buffering-hints {::fh/interval-in-seconds 9 44 | ::fh/size-in-mbs (xref :foo)} 45 | ::fh/compression-format "UNCOMPRESSED" 46 | ::fh/prefix (xref :foo) 47 | ::fh/role-arn (xref :foo)}})))) 48 | 49 | (testing "encode with kinesis source" 50 | (is (resource= {"Type" "AWS::KinesisFirehose::DeliveryStream", 51 | "Properties" {"DeliveryStreamName" {"Ref" "Foo"}, 52 | "KinesisStreamSourceConfiguration" 53 | {"KinesisStreamARN" {"Ref" "Foo"} 54 | "RoleARN" {"Ref" "Foo"}}}} 55 | (fh/firehose 56 | {::fh/delivery-stream-name (xref :foo) 57 | ::fh/kinesis-stream-source-configuration 58 | {::fh/kinesis-stream-arn (xref :foo) 59 | ::fh/role-arn (xref :foo)}}))))) 60 | -------------------------------------------------------------------------------- /src/crucible/aws/lambda.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.lambda 2 | (:require [crucible.resources :refer [spec-or-ref defresource]] 3 | [clojure.spec.alpha :as s])) 4 | 5 | (s/def ::subnet-id (s/* (spec-or-ref string?))) 6 | 7 | (s/def ::security-group-ids (s/* (spec-or-ref string?))) 8 | 9 | (s/def ::vpc-config (s/keys :req [::security-group-ids ::subnet-ids])) 10 | 11 | (s/def ::timeout (spec-or-ref (s/and pos-int? #(<= % (* 5 60))))) 12 | 13 | (s/def ::runtime #{"nodejs" 14 | "nodejs4.3" 15 | "nodejs6.10" 16 | "java8" 17 | "python2.7" 18 | "dotnetcore1.0" 19 | "nodejs4.3-edge"}) 20 | 21 | (s/def ::role (spec-or-ref string?)) 22 | 23 | (s/def ::memory-size (spec-or-ref (s/and #(-> % (mod 64) (= 0)) #(<= 128 % 1536)))) 24 | 25 | (s/def ::handler (spec-or-ref string?)) 26 | 27 | (s/def ::function-name (spec-or-ref string?)) 28 | 29 | (s/def ::description (spec-or-ref string?)) 30 | 31 | (s/def ::s3-bucket (spec-or-ref string?)) 32 | (s/def ::s3-key (spec-or-ref string?)) 33 | (s/def ::s3-object-version (spec-or-ref string?)) 34 | (s/def ::zip-file (spec-or-ref string?)) 35 | 36 | (s/def ::code (s/keys ::opt [::s3-bucket 37 | ::s3-key 38 | ::s3-object-version 39 | ::zip-file])) 40 | 41 | (s/def ::variables (s/map-of (s/or :kw keyword? :str string?) (spec-or-ref string?))) 42 | 43 | (s/def ::environment (s/keys :req [::variables])) 44 | 45 | (s/def ::reserved-concurrent-executions (spec-or-ref nat-int?)) 46 | 47 | (s/def ::function (s/keys :req [::code 48 | ::handler 49 | ::role 50 | ::runtime] 51 | :opt [::function-name 52 | ::description 53 | ::memory-size 54 | ::runtime 55 | ::vpc-config 56 | ::environment 57 | ::reserved-concurrent-executions 58 | ::timeout])) 59 | 60 | (defresource function "AWS::Lambda::Function" ::function) 61 | 62 | (s/def ::batch-size (spec-or-ref (s/and pos-int? #(< % 10000)))) 63 | 64 | (s/def ::enabled (spec-or-ref boolean?)) 65 | 66 | (s/def ::event-source-arn (spec-or-ref string?)) 67 | 68 | (s/def ::starting-position (spec-or-ref #{"TRIM_HORIZON" "LATEST"})) 69 | 70 | (s/def ::event-source-mapping (s/keys :req [::event-source-arn 71 | ::function-name] 72 | :opt [::batch-size 73 | ::enabled 74 | ::starting-position])) 75 | 76 | (defresource event-source-mapping "AWS::Lambda::EventSourceMapping" ::event-source-mapping) 77 | 78 | (s/def ::action (spec-or-ref string?)) 79 | 80 | (s/def ::function-name (spec-or-ref string?)) 81 | 82 | (s/def ::principal (spec-or-ref string?)) 83 | 84 | (s/def ::source-account (spec-or-ref string?)) 85 | 86 | (s/def ::source-arn (spec-or-ref string?)) 87 | 88 | (s/def ::permission (s/keys :req [::action 89 | ::function-name 90 | ::principal] 91 | :opt [::source-account 92 | ::source-arn])) 93 | 94 | (defresource permission "AWS::Lambda::Permission" ::permission) 95 | -------------------------------------------------------------------------------- /src/crucible/aws/ecs/service.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ecs.service 2 | "Resources in AWS::ECS::Service" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.encoding.keys :refer [->key]] 5 | [crucible.resources :refer [spec-or-ref]])) 6 | 7 | (s/def ::task-definition (spec-or-ref string?)) 8 | 9 | (s/def ::cluster (spec-or-ref string?)) 10 | 11 | (s/def ::maximum-percent (spec-or-ref integer?)) 12 | (s/def ::minimum-healthy-percent (spec-or-ref integer?)) 13 | 14 | (s/def ::deployment-configuration (s/keys :req [] 15 | :opt [::maximum-percent 16 | ::minimum-healthy-percent])) 17 | 18 | (s/def ::desired-count (spec-or-ref integer?)) 19 | 20 | (s/def ::health-check-grace-period-seconds (spec-or-ref integer?)) 21 | 22 | (s/def ::launch-type #{"EC2" "FARGATE"}) 23 | 24 | (s/def ::container-name (spec-or-ref string?)) 25 | (s/def ::container-port (spec-or-ref integer?)) 26 | (s/def ::load-balancer-name (spec-or-ref string?)) 27 | (s/def ::target-group-arn (spec-or-ref string?)) 28 | 29 | (s/def ::single-load-balancer (s/keys :req [] 30 | :opt [::container-name 31 | ::container-port 32 | ::load-balancer-name 33 | ::target-group-arn])) 34 | 35 | (s/def ::load-balancers (s/coll-of ::single-load-balancer :kind vector?)) 36 | 37 | 38 | (s/def ::subnets (s/coll-of (spec-or-ref string?) :kind vector?)) 39 | (s/def ::assign-public-ip #{"ENABLED" "DISABLED"}) 40 | (s/def ::security-groups (s/coll-of (spec-or-ref string?) :kind vector?)) 41 | 42 | 43 | (s/def ::aws-vpc-configuration (s/keys :req [::subnets] 44 | :opt [::assign-public-ip 45 | ::security-groups])) 46 | 47 | (defmethod ->key :aws-vpc-configuration [_] "AwsvpcConfiguration") 48 | 49 | (s/def ::network-configuration (s/keys :req [] 50 | :opt [::aws-vpc-configuration])) 51 | 52 | (s/def ::placement-constraint-type #{"distinctInstance" "memberOf"}) 53 | (s/def ::placement-constraint-expression (spec-or-ref string?)) 54 | 55 | (s/def ::placement-constraints (s/keys :req [::placement-constraint-type] 56 | :opt [::placement-constraint-expression])) 57 | 58 | (s/def ::placement-strategies-type #{"random" "spread" "binpack"}) 59 | (s/def ::placement-strategies-field (spec-or-ref string?)) 60 | 61 | (s/def ::placement-strategies (s/keys :req [::placement-strategies-type] 62 | :opt [::placement-strategies-field])) 63 | 64 | (s/def ::platform-version (spec-or-ref string?)) 65 | 66 | (s/def ::role (spec-or-ref string?)) 67 | 68 | (s/def ::service-name (spec-or-ref string?)) 69 | 70 | (s/def ::service (s/keys :req [::task-definition] 71 | :opt [::cluster 72 | ::deployment-configuration 73 | ::desired-count 74 | ::health-check-grace-period-seconds 75 | ::launch-type 76 | ::load-balancers 77 | ::network-configuration 78 | ::placement-constraints 79 | ::placement-strategies 80 | ::platform-version 81 | ::role 82 | ::service-name])) 83 | -------------------------------------------------------------------------------- /test-resources/aws/dynamodb/complex-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::DynamoDB::Table", 3 | "Properties": { 4 | "AttributeDefinitions": [ 5 | { 6 | "AttributeName": "Album", 7 | "AttributeType": "S" 8 | }, 9 | { 10 | "AttributeName": "Artist", 11 | "AttributeType": "S" 12 | }, 13 | { 14 | "AttributeName": "Sales", 15 | "AttributeType": "N" 16 | }, 17 | { 18 | "AttributeName": "NumberOfSongs", 19 | "AttributeType": "N" 20 | } 21 | ], 22 | "KeySchema": [ 23 | { 24 | "AttributeName": "Album", 25 | "KeyType": "HASH" 26 | }, 27 | { 28 | "AttributeName": "Artist", 29 | "KeyType": "RANGE" 30 | } 31 | ], 32 | "ProvisionedThroughput": { 33 | "ReadCapacityUnits": "5", 34 | "WriteCapacityUnits": "5" 35 | }, 36 | "TableName": "myTableName", 37 | "GlobalSecondaryIndexes": [ 38 | { 39 | "IndexName": "myGSI", 40 | "KeySchema": [ 41 | { 42 | "AttributeName": "Sales", 43 | "KeyType": "HASH" 44 | }, 45 | { 46 | "AttributeName": "Artist", 47 | "KeyType": "RANGE" 48 | } 49 | ], 50 | "Projection": { 51 | "NonKeyAttributes": [ 52 | "Album", 53 | "NumberOfSongs" 54 | ], 55 | "ProjectionType": "INCLUDE" 56 | }, 57 | "ProvisionedThroughput": { 58 | "ReadCapacityUnits": "5", 59 | "WriteCapacityUnits": "5" 60 | } 61 | }, 62 | { 63 | "IndexName": "myGSI2", 64 | "KeySchema": [ 65 | { 66 | "AttributeName": "NumberOfSongs", 67 | "KeyType": "HASH" 68 | }, 69 | { 70 | "AttributeName": "Sales", 71 | "KeyType": "RANGE" 72 | } 73 | ], 74 | "Projection": { 75 | "NonKeyAttributes": [ 76 | "Album", 77 | "Artist" 78 | ], 79 | "ProjectionType": "INCLUDE" 80 | }, 81 | "ProvisionedThroughput": { 82 | "ReadCapacityUnits": "5", 83 | "WriteCapacityUnits": "5" 84 | } 85 | } 86 | ], 87 | "LocalSecondaryIndexes": [ 88 | { 89 | "IndexName": "myLSI", 90 | "KeySchema": [ 91 | { 92 | "AttributeName": "Album", 93 | "KeyType": "HASH" 94 | }, 95 | { 96 | "AttributeName": "Sales", 97 | "KeyType": "RANGE" 98 | } 99 | ], 100 | "Projection": { 101 | "NonKeyAttributes": [ 102 | "Artist", 103 | "NumberOfSongs" 104 | ], 105 | "ProjectionType": "INCLUDE" 106 | } 107 | } 108 | ] 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/crucible/aws/rds.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.rds 2 | "Resources in AWS::RDS::*" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref defresource] :as res] 5 | [crucible.encoding.keys :refer [->key]] 6 | [crucible.aws.rds.db-subnet-group :as db-subnet-group] 7 | [crucible.aws.rds.db-instance :as dbi])) 8 | 9 | (defn rds [resource] (str "AWS::RDS::" (->key resource))) 10 | 11 | (defmethod ->key :db-cluster [_] "DBCluster") 12 | (defmethod ->key :db-instance [_] "DBInstance") 13 | (defmethod ->key :db-security-group [_] "DBSecurityGroup") 14 | (defmethod ->key :db-security-group-ingress [_] "DBSecurityGroupIngress") 15 | (defmethod ->key :db-subnet-group [_] "DBSubnetGroup") 16 | 17 | (s/def ::db-instance (s/keys :opt [::dbi/allocated-storage 18 | ::dbi/allow-major-version-upgrade 19 | ::dbi/auto-minor-version-upgrade 20 | ::dbi/availability-zone 21 | ::dbi/backup-retention-period 22 | ::dbi/character-set-name 23 | ::dbi/copy-tags-to-snapshot 24 | ::dbi/db-cluster-identifier 25 | ::dbi/db-instance-class 26 | ::dbi/db-instance-identifier 27 | ::dbi/db-name 28 | ::dbi/db-parameter-group-name 29 | ::dbi/db-security-groups 30 | ::dbi/db-snapshot-identifier 31 | ::dbi/db-snapshot-group-name 32 | ::dbi/domain 33 | ::dbi/domain-iam-role-name 34 | ::dbi/engine 35 | ::dbi/engine-version 36 | ::dbi/iops 37 | ::dbi/kms-key-id 38 | ::dbi/license-model 39 | ::dbi/master-username 40 | ::dbi/master-username-password 41 | ::dbi/monitoring-interval 42 | ::dbi/monitoring-role-arn 43 | ::dbi/multi-az 44 | ::dbi/option-group-name 45 | ::dbi/port 46 | ::dbi/preferred-backup-window 47 | ::dbi/preferred-maintenance-window 48 | ::dbi/publicly-accessible 49 | ::dbi/source-db-instance-identifier 50 | ::dbi/storage-encrypted 51 | ::dbi/storage-type 52 | ::dbi/tags 53 | ::dbi/timezone 54 | ::dbi/vpc-security-groups])) 55 | 56 | (defresource db-instance (rds :db-instance) ::db-instance) 57 | 58 | (s/def ::db-cluster any?) 59 | (defresource db-cluster (rds :db-cluster) ::db-cluster) 60 | 61 | (s/def ::db-security-group any?) 62 | (defresource db-security-group (rds :db-security-group) ::db-security-group) 63 | 64 | (s/def ::db-security-group-ingress any?) 65 | (defresource db-security-group-ingress (rds :db-security-group-ingress) ::db-security-group-ingress) 66 | 67 | (defresource db-subnet-group (rds :db-subnet-group) ::db-subnet-group/db-subnet-group-spec) 68 | 69 | (s/def ::event-subscription any?) 70 | (defresource event-subscription (rds :event-subscription) ::event-subscription) 71 | 72 | (s/def ::option-group any?) 73 | (defresource option-group (rds :option-group) ::option-group) 74 | -------------------------------------------------------------------------------- /test/crucible/aws/s3_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.s3-test 2 | (:require [crucible.core :refer [parameter xref]] 3 | [crucible.assertion :refer [resource=]] 4 | [crucible.aws.s3 :as s3] 5 | [crucible.aws.s3.bucket-encryption :as bucket-encryption] 6 | [crucible.aws.iam :as iam] 7 | [cheshire.core :as json] 8 | [clojure.spec.alpha :as s] 9 | [clojure.test :refer :all] 10 | [clojure.java.io :as io])) 11 | 12 | (deftest s3-bucket-policy-test 13 | (testing "encode" 14 | (is (resource= {"Type" "AWS::S3::BucketPolicy", 15 | "Properties" 16 | {"Bucket" {"Ref" "Foo"}, 17 | "PolicyDocument" 18 | {"Statement" 19 | [{"Action" ["s3:GetObject"], 20 | "Effect" "Allow", 21 | "Principal" {"Service" "lambda.amazonaws.com"}, 22 | "Condition" {"StringEquals" {"aws:SourceArn" {"Ref" "Foo"}}}, 23 | "Resource" {"Ref" "Foo"}}]}}} 24 | (s3/bucket-policy 25 | {::s3/bucket (xref :foo) 26 | ::iam/policy-document 27 | {::iam/statement 28 | [{::iam/action ["s3:GetObject"] 29 | ::iam/effect "Allow" 30 | ::iam/principal {::iam/service "lambda.amazonaws.com"} 31 | ::iam/condition {"StringEquals" {"aws:SourceArn" (xref :foo)}} 32 | ::iam/resource (xref :foo)}]}}))))) 33 | 34 | (deftest s3-cors-test 35 | (testing "encode" 36 | (is (resource= (json/decode (slurp (io/resource "aws/s3/s3-cors.json"))) 37 | (s3/bucket 38 | {::s3/access-control "PublicReadWrite" 39 | ::s3/cors-configuration {::s3/cors-rules 40 | [{::s3/allowed-headers ["*"] 41 | ::s3/allowed-methods ["GET"] 42 | ::s3/allowed-origins ["*"] 43 | ::s3/exposed-headers ["Date"] 44 | ::s3/id "myCORSRuleId1" 45 | ::s3/max-age 3600} 46 | {::s3/allowed-headers ["x-amz-*"] 47 | ::s3/allowed-methods ["DELETE"] 48 | ::s3/allowed-origins ["http://www.example1.com" 49 | "http://www.example2.com"] 50 | ::s3/exposed-headers ["Connection" 51 | "Server" 52 | "Date"] 53 | ::s3/id "myCORSRuleId2" 54 | ::s3/max-age 1800}]}}))))) 55 | 56 | (deftest bucket-encryption 57 | (testing "bucket-encryption" 58 | (is (s/valid? ::s3/s3-bucket 59 | {::s3/bucket-name "secret-docs" 60 | ::s3/bucket-encryption 61 | {::bucket-encryption/server-side-encryption-configuration 62 | [{::bucket-encryption/server-side-encryption-by-default 63 | {::bucket-encryption/kms-master-key-id "1234" 64 | ::bucket-encryption/sse-algorithm "aws:kms"}}]}})) 65 | (is (not (s/explain ::s3/s3-bucket 66 | {::s3/bucket-name "secret-docs" 67 | ::s3/bucket-encryption 68 | {::bucket-encryption/server-side-encryption-configuration 69 | [{::bucket-encryption/server-side-encryption-by-default 70 | {::bucket-encryption/kms-master-key-id "1234" 71 | ::bucket-encryption/sse-algorithm "AES256"}}]}}))))) 72 | -------------------------------------------------------------------------------- /test/crucible/encoding/serverless_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.encoding.serverless-test 2 | (:require [crucible.aws.kinesis :as k] 3 | [crucible.aws.serverless.function :as f] 4 | [crucible.aws.serverless.function.event-source :as es] 5 | [crucible.aws.serverless.function.event-source.kinesis :as es.k] 6 | [crucible.aws.serverless.globals :as g] 7 | [crucible.core :refer [xref template]] 8 | [crucible.encoding.serverless :as encoding.sam] 9 | [clojure.test :refer :all])) 10 | 11 | (deftest serverless-test 12 | (testing "encode without globals" 13 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 14 | "Transform" "AWS::Serverless-2016-10-31" 15 | "Description" "A function that processes data from a Kinesis stream." 16 | "Resources" {"StreamProcessor" 17 | {"Type" "AWS::Serverless::Function" 18 | "Properties" 19 | {"Handler" "index.handler" 20 | "Runtime" "nodejs6.10" 21 | "CodeUri" "src/" 22 | "Events" {"Stream" {"Type" "Kinesis" 23 | "Properties" {"Stream" {"Fn::GetAtt" ["Stream" "Arn"]} 24 | "StartingPosition" "TRIM_HORIZON"}}}}} 25 | 26 | "Stream" 27 | {"Type" "AWS::Kinesis::Stream" 28 | "Properties" {"ShardCount" 1}}}} 29 | (encoding.sam/build 30 | (template {:stream-processor 31 | (f/function 32 | {::f/handler "index.handler" 33 | ::f/runtime "nodejs6.10" 34 | ::f/code-uri "src/" 35 | ::f/events {:stream 36 | {::es/type "Kinesis" 37 | ::es.k/properties 38 | {::es.k/stream (xref :stream :arn) 39 | ::es.k/starting-position "TRIM_HORIZON"}}}}) 40 | :stream (k/stream 41 | {::k/shard-count 1})} 42 | "A function that processes data from a Kinesis stream."))))) 43 | 44 | (testing "encode with globals" 45 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 46 | "Transform" "AWS::Serverless-2016-10-31" 47 | "Description" "A function that processes data from a Kinesis stream." 48 | "Globals" {"Function" {"MemorySize" 1024 49 | "Timeout" 15}} 50 | "Resources" {"StreamProcessor" 51 | {"Type" "AWS::Serverless::Function" 52 | "Properties" 53 | {"Handler" "index.handler" 54 | "Runtime" "nodejs6.10" 55 | "CodeUri" "src/" 56 | "Events" {"Stream" {"Type" "Kinesis" 57 | "Properties" {"Stream" {"Fn::GetAtt" ["Stream" "Arn"]} 58 | "StartingPosition" "TRIM_HORIZON"}}}}} 59 | 60 | "Stream" 61 | {"Type" "AWS::Kinesis::Stream" 62 | "Properties" {"ShardCount" 1}}}} 63 | (encoding.sam/build 64 | (template {:stream-processor 65 | (f/function 66 | {::f/handler "index.handler" 67 | ::f/runtime "nodejs6.10" 68 | ::f/code-uri "src/" 69 | ::f/events {:stream 70 | {::es/type "Kinesis" 71 | ::es.k/properties 72 | {::es.k/stream (xref :stream :arn) 73 | ::es.k/starting-position "TRIM_HORIZON"}}}}) 74 | :stream (k/stream 75 | {::k/shard-count 1})} 76 | "A function that processes data from a Kinesis stream.") 77 | (g/globals 78 | {::g/function {::f/memory-size 1024 79 | ::f/timeout 15}})))))) 80 | -------------------------------------------------------------------------------- /test/crucible/aws/dynamodb_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.dynamodb-test 2 | (:require [crucible.aws.dynamodb :as ddb] 3 | [crucible.assertion :refer [resource=]] 4 | [crucible.core :refer [xref]] 5 | [cheshire.core :as json] 6 | [clojure.test :refer :all] 7 | [clojure.java.io :as io])) 8 | 9 | (deftest minimal-ddb-test 10 | (testing "encode" 11 | (is (resource= 12 | {"Type" "AWS::DynamoDB::Table", 13 | "Properties" 14 | {"AttributeDefinitions" 15 | [{"AttributeName" "foo", "AttributeType" "S"}], 16 | "KeySchema" [{"AttributeName" "foo", "KeyType" "HASH"}], 17 | "ProvisionedThroughput" 18 | {"ReadCapacityUnits" "20", 19 | "WriteCapacityUnits" {"Ref" "Param"}}}} 20 | (ddb/table {::ddb/attribute-definitions [{::ddb/attribute-name "foo" 21 | ::ddb/attribute-type "S"}] 22 | ::ddb/key-schema [{::ddb/attribute-name "foo" 23 | ::ddb/key-type "HASH"}] 24 | ::ddb/provisioned-throughput {::ddb/read-capacity-units "20" 25 | ::ddb/write-capacity-units (xref :param)}}))))) 26 | 27 | (deftest doc-example-test 28 | (testing "encode" 29 | (is (resource= 30 | (json/decode (slurp (io/resource "aws/dynamodb/complex-table.json"))) 31 | (ddb/table 32 | {::ddb/table-name "myTableName" 33 | 34 | ::ddb/attribute-definitions [{::ddb/attribute-name "Album" 35 | ::ddb/attribute-type "S"} 36 | {::ddb/attribute-name "Artist" 37 | ::ddb/attribute-type "S"} 38 | {::ddb/attribute-name "Sales" 39 | ::ddb/attribute-type "N"} 40 | {::ddb/attribute-name "NumberOfSongs" 41 | ::ddb/attribute-type "N"}] 42 | 43 | ::ddb/key-schema [{::ddb/attribute-name "Album" 44 | ::ddb/key-type "HASH"} 45 | {::ddb/attribute-name "Artist" 46 | ::ddb/key-type "RANGE"}] 47 | 48 | ::ddb/provisioned-throughput {::ddb/read-capacity-units "5" 49 | ::ddb/write-capacity-units "5"} 50 | 51 | ::ddb/global-secondary-indexes 52 | [{::ddb/index-name "myGSI" 53 | ::ddb/key-schema [{::ddb/attribute-name "Sales" 54 | ::ddb/key-type "HASH"} 55 | {::ddb/attribute-name "Artist" 56 | ::ddb/key-type "RANGE"}] 57 | ::ddb/projection {::ddb/non-key-attributes ["Album" 58 | "NumberOfSongs"] 59 | ::ddb/projection-type "INCLUDE"} 60 | ::ddb/provisioned-throughput {::ddb/read-capacity-units "5" 61 | ::ddb/write-capacity-units "5"}} 62 | {::ddb/index-name "myGSI2" 63 | ::ddb/key-schema [{::ddb/attribute-name "NumberOfSongs" 64 | ::ddb/key-type "HASH"} 65 | {::ddb/attribute-name "Sales" 66 | ::ddb/key-type "RANGE"}] 67 | ::ddb/projection {::ddb/non-key-attributes ["Album" 68 | "Artist"] 69 | ::ddb/projection-type "INCLUDE"} 70 | ::ddb/provisioned-throughput {::ddb/read-capacity-units "5" 71 | ::ddb/write-capacity-units "5"}}] 72 | :local-secondary-indexes 73 | [{::ddb/index-name "myLSI" 74 | ::ddb/key-schema [{::ddb/attribute-name "Album" 75 | ::ddb/key-type "HASH"} 76 | {::ddb/attribute-name "Sales" 77 | ::ddb/key-type "RANGE"}] 78 | ::ddb/projection {::ddb/non-key-attributes ["Artist" 79 | "NumberOfSongs"] 80 | ::ddb/projection-type "INCLUDE"}}]}))))) 81 | -------------------------------------------------------------------------------- /src/crucible/aws/auto_scaling/auto_scaling_group.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.auto-scaling.auto-scaling-group 2 | "AWS::AutoScaling::AutoScalingGroup" 3 | (:require [clojure.spec.alpha :as s] 4 | [crucible.resources :refer [spec-or-ref] :as res] 5 | [crucible.encoding.keys :refer [->key]])) 6 | 7 | (defmethod ->key :cool-down [_] "Cooldown") 8 | 9 | (s/def ::default-result (spec-or-ref string?)) 10 | (s/def ::heartbeat-timeout (spec-or-ref string?)) 11 | (s/def ::lifecycle-hook-name (spec-or-ref string?)) 12 | (s/def ::lifecycle-transition (spec-or-ref string?)) 13 | (s/def ::notification-metadata (spec-or-ref string?)) 14 | (s/def ::notification-target-arn (spec-or-ref string?)) 15 | (s/def ::role-arn (spec-or-ref string?)) 16 | 17 | (s/def ::granularity (spec-or-ref string?)) 18 | (s/def ::metric (spec-or-ref string?)) 19 | (s/def ::metrics (s/* ::metric)) 20 | 21 | (s/def ::notification-type (spec-or-ref string?)) 22 | (s/def ::notification-types (s/* ::notification-type)) 23 | (s/def ::topic-arn (spec-or-ref string?)) 24 | 25 | (s/def ::min-size (spec-or-ref string?)) 26 | (s/def ::max-size (spec-or-ref string?)) 27 | (s/def ::availability-zone (spec-or-ref string?)) 28 | (s/def ::availability-zones (s/* ::availability-zone)) 29 | (s/def ::cool-down (spec-or-ref string?)) 30 | (s/def ::desired-capacity (spec-or-ref string?)) 31 | (s/def ::health-check-grace-period (spec-or-ref int?)) 32 | (s/def ::health-check-type (spec-or-ref string?)) 33 | (s/def ::instance-id (spec-or-ref string?)) 34 | (s/def ::desired-capacity (spec-or-ref string?)) 35 | (s/def ::launch-configuration-name (spec-or-ref string?)) 36 | (s/def ::lifecycle-hook-specification (s/keys :req [::lifecycle-hook-name 37 | ::lifecycle-transition] 38 | :opt [::default-result 39 | ::heartbeat-timeout 40 | ::notification-metadata 41 | ::notification-target-arn 42 | ::role-arn]) ) 43 | (s/def ::lifecycle-hook-specification-list (s/* ::lifecycle-hook-specification)) 44 | (s/def ::load-balancer-name (spec-or-ref string?)) 45 | (s/def ::load-balancer-names (s/* ::load-balancer-name)) 46 | (s/def ::metric-collection (s/keys :req [::granularity] 47 | :opt [::metrics])) 48 | (s/def ::metrics-collection (s/* ::metric-collection)) 49 | (s/def ::notification-configuration (s/keys :req [::notification-types 50 | ::topic-arn])) 51 | (s/def ::notification-configurations (s/* ::metrics-collection)) 52 | (s/def ::placement-group (spec-or-ref string?)) 53 | (s/def ::target-group-arn (spec-or-ref string?)) 54 | (s/def ::target-group-arns (s/* ::target-group-arn)) 55 | (s/def ::termination-policy (spec-or-ref string?)) 56 | (s/def ::termination-policies (s/* ::termination-policy)) 57 | (s/def ::vpc-zone-id (spec-or-ref string?)) 58 | (s/def ::v-p-c-zone-identifier (s/* ::vpc-zone-id)) 59 | 60 | (s/def ::key string?) 61 | (s/def ::resource-type string?) 62 | (s/def ::resource-id string?) 63 | (s/def ::propagate-at-launch boolean?) 64 | (s/def ::value (spec-or-ref string?)) 65 | (s/def ::tag (s/keys :req [::key ::value] 66 | :opt [::resource-id ::resource-type ::propagate-at-launch])) 67 | (s/def ::tags (s/* ::tag)) 68 | 69 | (s/def ::resource-spec (s/keys :req [::max-size 70 | ::min-size] 71 | :opt [::availability-zones 72 | ::cool-down 73 | ::desired-capacity 74 | ::health-check-type 75 | ::health-check-grace-period 76 | ::instance-id 77 | ::desired-capacity 78 | ::launch-configuration-name 79 | ::life-cycle-hook-specification-list 80 | ::load-balancer-names 81 | ::metric-collections 82 | ::notification-configurations 83 | ::placement-group 84 | ::target-group-arns 85 | ::termination-policies 86 | ::v-p-c-zone-identifier 87 | ::tags 88 | ::res/tags])) 89 | -------------------------------------------------------------------------------- /src/crucible/aws/s3.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.s3 2 | "Resources in AWS::S3::*" 3 | (:require [crucible.resources :refer [spec-or-ref defresource] :as res] 4 | [crucible.aws.iam :as iam] 5 | [crucible.aws.s3.bucket-encryption :as bucket-encryption] 6 | [clojure.spec.alpha :as s])) 7 | 8 | (s/def ::arn string?) 9 | 10 | (s/def ::value (spec-or-ref string?)) 11 | 12 | (s/def ::name (spec-or-ref #{"prefix" "suffix"})) 13 | 14 | (s/def ::rule (s/keys :req [::name ::value])) 15 | 16 | (s/def ::rules (s/coll-of ::rule :kind vector?)) 17 | 18 | (s/def ::s3-key (s/keys :req [::rules])) 19 | 20 | (s/def ::filter (s/keys :req [::s3-key])) 21 | 22 | (s/def ::event (spec-or-ref #{"s3:ObjectCreated:*" 23 | "s3:ObjectCreated:Put" 24 | "s3:ObjectCreated:Post" 25 | "s3:ObjectCreated:Copy" 26 | "s3:ObjectCreated:CompleteMultipartUpload" 27 | "s3:ObjectRemoved:*" 28 | "s3:ObjectRemoved:Delete" 29 | "s3:ObjectRemoved:DeleteMarkerCreated" 30 | "s3:ReducedRedundancyLostObject"})) 31 | 32 | (s/def ::topic (spec-or-ref ::arn)) 33 | 34 | (s/def ::topic-configuration (s/keys :req [::event 35 | ::topic] 36 | :opt [::filter])) 37 | 38 | (s/def ::topic-configurations (s/coll-of ::topic-configuration :kind vector?)) 39 | 40 | (s/def ::queue (spec-or-ref ::arn)) 41 | 42 | (s/def ::queue-configuration (s/keys :req [::event 43 | ::queue] 44 | :opt [::filter])) 45 | 46 | (s/def ::queue-configurations (s/coll-of ::queue-configuration :kind vector?)) 47 | 48 | (s/def ::lambda-configuration (s/keys :req [::event 49 | ::function] 50 | :opt [::filter])) 51 | (s/def ::function (spec-or-ref ::arn)) 52 | 53 | (s/def ::lambda-configurations (s/coll-of ::lambda-configuration :kind vector?)) 54 | 55 | (s/def ::notification-configuration (s/keys :opt [::lambda-configurations 56 | ::queue-configurations 57 | ::topic-configurations])) 58 | 59 | (s/def ::max-age (spec-or-ref pos-int?)) 60 | 61 | (s/def ::id (spec-or-ref (s/and string? 62 | #(< (count %) 256)))) 63 | 64 | (s/def ::exposed-headers (s/coll-of (spec-or-ref string?) :kind vector?)) 65 | 66 | (s/def ::allowed-origins (s/coll-of (spec-or-ref string?) :kind vector?)) 67 | 68 | (s/def ::allowed-headers (s/coll-of (spec-or-ref string?) :kind vector?)) 69 | 70 | (s/def ::allowed-methods (s/coll-of (spec-or-ref #{"GET" "PUT" "HEAD" "POST" "DELETE"}) 71 | :kind vector)) 72 | 73 | (s/def ::cors-rule (s/keys :req [::allowed-methods 74 | ::allowed-origins] 75 | :opt [::allowed-headers 76 | ::exposed-headers 77 | ::id 78 | ::max-age])) 79 | 80 | (s/def ::cors-rules (s/coll-of ::cors-rule :kind vector?)) 81 | 82 | (s/def ::cors-configuration (s/keys :req [::cors-rules])) 83 | 84 | (s/def ::bucket-name (spec-or-ref (s/and string? 85 | #(re-matches #"[a-z0-9-.]+" %)))) 86 | 87 | (s/def ::bucket-encryption (spec-or-ref ::bucket-encryption/resource-property-spec)) 88 | 89 | (s/def ::access-control #{"AuthenticatedRead" 90 | "AwsExecRead" 91 | "BucketOwnerRead" 92 | "BucketOwnerFullControl" 93 | "LogDeliveryWrite" 94 | "Private" 95 | "PublicRead" 96 | "PublicReadWrite"}) 97 | 98 | (s/def ::s3-bucket (s/keys :opt [::bucket-name 99 | ::bucket-encryption 100 | ::access-control 101 | ::cors-configuration 102 | ::lifecycle-configuration 103 | ::logging-configuration 104 | ::notification-configuration 105 | ::replication-configuration 106 | ::res/tags 107 | ::versioning-configuration 108 | ::website-configuration])) 109 | 110 | (defresource bucket "AWS::S3::Bucket" ::s3-bucket) 111 | 112 | (s/def ::bucket (spec-or-ref string?)) 113 | 114 | (s/def ::bucket-policy (s/keys :req [::bucket 115 | ::iam/policy-document])) 116 | 117 | (defresource bucket-policy "AWS::S3::BucketPolicy" ::bucket-policy) 118 | -------------------------------------------------------------------------------- /src/crucible/core.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.core 2 | "Commonly used template construction functions" 3 | (:require [clojure.spec.alpha :as s] 4 | [clojure.walk :as walk] 5 | [crucible.values :as v] 6 | [crucible.parameters :as p] 7 | [crucible.resources :as r] 8 | [crucible.outputs :as o] 9 | [crucible.encoding :as encoding] 10 | [expound.alpha :as expound])) 11 | 12 | (s/def ::description string?) 13 | 14 | (s/def ::element (s/cat :type #{:parameter 15 | :condition 16 | :mapping 17 | :resource 18 | :output} 19 | :specification any?)) 20 | 21 | (s/def ::template (s/cat :description ::description 22 | :elements (s/nilable (s/map-of keyword? ::element)))) 23 | 24 | (defn validate 25 | "Ensure the template is structurally valid, for example xref'd elements exist" 26 | [template] 27 | (walk/prewalk 28 | (fn [x] 29 | (cond 30 | (and (vector? x) 31 | (= 2 (count x)) 32 | (= ::v/ref (first x))) (if-not (contains? (:elements template) (second x)) 33 | (throw (ex-info "Missing reference" {:ref (second x)})) 34 | x) 35 | :else x)) 36 | template)) 37 | 38 | (defn template 39 | "Make a template structure with the given description and elements" 40 | ([elements description] 41 | {:pre [(map? elements) 42 | (string? description)]} 43 | (let [input [description elements] 44 | spec ::template 45 | parsed (s/conform spec input)] 46 | (if (= parsed ::s/invalid) 47 | (throw (ex-info (str "Invalid input" (expound/expound-str spec input)) 48 | (s/explain-data spec input))) 49 | (-> parsed 50 | validate 51 | (with-meta {::template true}))))) 52 | ([description first-key first-val & {:as others}] 53 | (let [elements (assoc others first-key first-val)] 54 | (template elements description)))) 55 | 56 | (defn parameter 57 | "Make a template parameter element" 58 | [& {:keys [::p/type] 59 | :or {type ::p/string} 60 | :as options}] 61 | [:parameter (assoc options ::p/type type)]) 62 | 63 | (defn condition 64 | "Make a template condition element" 65 | [value] 66 | [:condition value]) 67 | 68 | (defn mapping 69 | "Make a template mapping element" 70 | [& {:as keymaps}] 71 | [:mapping keymaps]) 72 | 73 | (defn resource 74 | "Make a template resource element" 75 | [options] 76 | [:resource options]) 77 | 78 | (defn output 79 | "Make a template output with the value and an optional description and export name" 80 | [value & [description export-name]] 81 | [:output (-> description 82 | (when {::o/description description}) 83 | (merge {::o/value value}) 84 | (merge (when export-name {::o/export {::o/name export-name}})))]) 85 | 86 | (defn xref "Cross-reference another template element, optionally 87 | specifying a resource attribute. Produces Ref and Fn::GetAtt." 88 | ([xref] 89 | (v/xref xref)) 90 | ([xref att] 91 | (v/xref xref att))) 92 | 93 | (defn join 94 | "Join values at template application time with an optional 95 | delimiter. See Fn::Join." 96 | ([values] 97 | (join "" values)) 98 | ([delimiter values] 99 | (v/join delimiter values))) 100 | 101 | (defn fn-if 102 | "See Fn:If" 103 | [condition true-value false-value] 104 | (v/fn-if condition true-value false-value)) 105 | 106 | (defn select 107 | "Select a value from a list at template application time. See 108 | Fn::Select" 109 | [index values] 110 | (v/select index values)) 111 | 112 | (defn equals 113 | "See Fn::Equals" 114 | [x y] 115 | (v/equals x y)) 116 | 117 | (defn find-in-map 118 | "Returns the value corresponding to keys in a two-level map that is declared 119 | in the Mappings section" 120 | [map-name top-level-key second-level-key] 121 | (v/find-in-map map-name top-level-key second-level-key)) 122 | 123 | (defn import-value 124 | "Import an exported value" 125 | [value-name] 126 | (v/import-value value-name)) 127 | 128 | (defn sub 129 | "Interpolate values from a template string" 130 | [string-to-interpolate] 131 | (v/sub string-to-interpolate)) 132 | 133 | (defn base64 134 | "The intrinsic function Fn::Base64 returns the Base64 representation of the input string." 135 | [input-string] 136 | (v/base64 input-string)) 137 | 138 | (defn encode 139 | "Encode a template into JSON for use by CloudFormation" 140 | [template] 141 | (encoding/encode template)) 142 | 143 | (def account-id (v/pseudo ::v/account-id)) 144 | (def notification-arns (v/pseudo ::v/notification-arns)) 145 | (def no-value (v/pseudo ::v/no-value)) 146 | (def region (v/pseudo ::v/region)) 147 | (def stack-id (v/pseudo ::v/stack-id)) 148 | (def stack-name (v/pseudo ::v/stack-name)) 149 | -------------------------------------------------------------------------------- /test/crucible/aws/iam_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.iam-test 2 | (:require [crucible.aws.iam :as iam] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :refer :all] 5 | [crucible.core :refer [xref] :as cf])) 6 | 7 | (defn valid [spec candidate] 8 | (nil? (s/explain-data spec candidate))) 9 | 10 | (deftest principal-tests 11 | 12 | (testing "everyone via *" (is (valid ::iam/principal "*"))) 13 | 14 | (testing "everyone via {AWS *}" (is (valid ::iam/principal {::iam/aws "*"}))) 15 | 16 | (testing "aws account" (is (valid ::iam/principal {::iam/aws "foo"}))) 17 | 18 | (testing "list of aws accounts" (is (valid ::iam/principal {::iam/aws ["foo" "bar"]}))) 19 | 20 | (testing "federated identity" (is (valid ::iam/principal {::iam/federated "graph.facebook.com"}))) 21 | 22 | (testing "federated identity single" 23 | (is (valid ::iam/principal {::iam/service "ec2.amazonaws.com"}))) 24 | 25 | (testing "federated identity" 26 | (is (valid ::iam/principal {::iam/service ["ec2.amazonaws.com" 27 | "datapipeline.amazonaws.com"]}))) 28 | 29 | (testing "federated identity string" 30 | (is (valid ::iam/principal {"Service" "ec2.amazonaws.com"}))) 31 | 32 | (testing "canonical user" (is (valid ::iam/principal {::iam/canonical-user "foo"}))) 33 | 34 | (testing "not principal" 35 | (is (valid ::iam/not-principal {::iam/service ["ec2.amazonaws.com" 36 | "datapipeline.amazonaws.com"]})))) 37 | 38 | (deftest action-tests 39 | 40 | (testing "hyphen in action" (is (valid ::iam/action "execute-api:Invoke"))) 41 | 42 | (testing "all actions" (is (valid ::iam/action "*"))) 43 | 44 | (testing "single action" (is (valid ::iam/action "s3:PutObject"))) 45 | 46 | (testing "multiple actions" (is (valid ::iam/action ["s3:PutObject" "s3:DeleteObject"]))) 47 | 48 | (testing "not action" (is (valid ::iam/not-action "s3:PutObject")))) 49 | 50 | (deftest resource-tests 51 | 52 | (testing "all resources" (is (valid ::iam/resource "*"))) 53 | 54 | (testing "single resource" (is (valid ::iam/resource "foo"))) 55 | 56 | (testing "multiple resources" (is (valid ::iam/resource ["foo" "bar"]))) 57 | 58 | (testing "not resource" (is (valid ::iam/not-resource "foo")))) 59 | 60 | (deftest role-tests 61 | 62 | (testing "simple role" 63 | (is (valid ::iam/role 64 | {::iam/assume-role-policy-document 65 | {::iam/version "2012-10-17" 66 | ::iam/statement [{::iam/effect "Allow" 67 | ::iam/principal {::iam/service ["ecs-tasks.amazonaws.com"]} 68 | ::iam/action ["sts:AssumeRole"]}]}})))) 69 | 70 | (deftest condition-tests 71 | 72 | (testing "single condition" 73 | (is (valid ::iam/condition {:date-greater-than 74 | {"aws:CurrentTime" "2013-08-16T12:00:00Z"}}))) 75 | 76 | (testing "single condition string" 77 | (is (valid ::iam/condition {"DateGreaterThan" 78 | {"aws:CurrentTime" "2013-08-16T12:00:00Z"}})))) 79 | 80 | (deftest user-tests 81 | (testing "empty user" 82 | (is (valid ::iam/user {})))) 83 | 84 | (deftest policy-tests 85 | (testing "attaching a policy to a user" 86 | (is (valid ::iam/policy {::iam/policy-name "db-access" 87 | ::iam/users [(xref :user1) (xref :user2)] 88 | ::iam/policy-document {::iam/version "2012-10-17" 89 | ::iam/statement [{::iam/action ["dynamodb:*"] 90 | ::iam/effect "Allow" 91 | ::iam/resource "arn:aws:dynamodb:*:..."}]}}))) 92 | (testing "attaching a policy to a group" 93 | (is (valid ::iam/policy {::iam/policy-name "db-access" 94 | ::iam/groups [(xref :group1) (xref :group2)] 95 | ::iam/policy-document {::iam/version "2012-10-17" 96 | ::iam/statement [{::iam/action ["dynamodb:*"] 97 | ::iam/effect "Allow" 98 | ::iam/resource "arn:aws:dynamodb:*:..."}]}}))) 99 | (testing "attaching a policy to a role" 100 | (is (valid ::iam/policy {::iam/policy-name "db-access" 101 | ::iam/roles [(xref :transactor-role) (xref :backup-transactor-role)] 102 | ::iam/policy-document {::iam/version "2012-10-17" 103 | ::iam/statement [{::iam/action ["dynamodb:*"] 104 | ::iam/effect "Allow" 105 | ::iam/resource "arn:aws:dynamodb:*:..."}]}})))) 106 | -------------------------------------------------------------------------------- /src/crucible/values.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.values 2 | (:require [clojure.spec.alpha :as s] 3 | [crucible.resources :refer [spec-or-ref]] 4 | [crucible.encoding.keys :as keys])) 5 | 6 | (s/def ::ref keyword?) 7 | (s/def ::att keyword?) 8 | (s/def ::delimiter string?) 9 | 10 | (s/def ::param #{::account-id 11 | ::notification-arns 12 | ::no-value 13 | ::region 14 | ::stack-id 15 | ::stack-name}) 16 | 17 | (defmulti value-type ::type) 18 | 19 | (s/def ::value (s/multi-spec value-type ::type)) 20 | (s/def ::values (s/+ ::value)) 21 | 22 | (s/def ::index (s/and integer? 23 | #(>= % 0))) 24 | 25 | (s/def ::xref (s/keys :req [::type ::ref] 26 | :opt [::att])) 27 | 28 | (defmethod value-type ::xref [_] ::xref) 29 | 30 | (s/def ::pseudo (s/keys :req [::type ::param])) 31 | 32 | (defmethod value-type ::pseudo [_] ::pseudo) 33 | 34 | (s/def ::fn-value (spec-or-ref string?)) 35 | 36 | (s/def ::fn-values (s/coll-of ::fn-value :kind vector?)) 37 | 38 | (s/def ::join (s/keys :req [::type ::fn-values] 39 | :opt [::delimiter])) 40 | 41 | (defmethod value-type ::join [_] ::join) 42 | 43 | (s/def ::condition-name (spec-or-ref string?)) 44 | 45 | (s/def ::value-if-true (spec-or-ref string?)) 46 | 47 | (s/def ::value-if-false (spec-or-ref string?)) 48 | 49 | (s/def ::if (s/keys :req [::type ::condition-name ::value-if-true ::value-if-false])) 50 | 51 | (defmethod value-type ::if [_] ::if) 52 | 53 | (s/def ::select (s/keys :req [::type ::fn-values ::index])) 54 | 55 | (defmethod value-type ::select [_] ::select) 56 | 57 | (s/def ::map-name (spec-or-ref keyword?)) 58 | 59 | (s/def ::top-level-key (spec-or-ref string?)) 60 | 61 | (s/def ::second-level-key (spec-or-ref string?)) 62 | 63 | (s/def ::find-in-map (s/keys :req [::type 64 | ::map-name 65 | ::top-level-key 66 | ::second-level-key])) 67 | 68 | (defmethod value-type ::find-in-map [_] ::find-in-map) 69 | 70 | 71 | 72 | (defmulti encode-value ::type) 73 | 74 | (defmethod encode-value :default [x] x) 75 | 76 | (defmethod encode-value ::xref [{:keys [::ref ::att]}] 77 | (if att 78 | {"Fn::GetAtt" [(keys/->key ref) (keys/->key att)]} 79 | {"Ref" (keys/->key ref)})) 80 | 81 | (defmethod keys/->key :notification-arns [_] 82 | "NotificationARNs") 83 | 84 | (defmethod encode-value ::pseudo [{:keys [::param]}] 85 | {"Ref" (str "AWS::" (-> param name keyword keys/->key))}) 86 | 87 | (defmethod encode-value ::join [{:keys [::delimiter ::fn-values]}] 88 | {"Fn::Join" [(or delimiter "") (vec (map encode-value fn-values))]}) 89 | 90 | (defmethod encode-value ::if [{::keys [condition-name value-if-true value-if-false]}] 91 | {"Fn::If" [condition-name value-if-true value-if-false]}) 92 | 93 | (defmethod encode-value ::select [{:keys [::index ::fn-values]}] 94 | {"Fn::Select" [(str index) (vec (map encode-value fn-values))]}) 95 | 96 | (defmethod encode-value ::equals [{:keys [::x ::y]}] 97 | {"Fn::Equals" [x y]}) 98 | 99 | (defmethod encode-value ::base64 [{:keys [::input-string]}] 100 | {"Fn::Base64" input-string}) 101 | 102 | (defmethod encode-value ::find-in-map [{:keys [::map-name 103 | ::top-level-key 104 | ::second-level-key]}] 105 | {"Fn::FindInMap" 106 | [(keys/->key map-name) 107 | (encode-value top-level-key) 108 | (encode-value second-level-key)]}) 109 | 110 | (defn xref 111 | ([xref] 112 | {::type ::xref ::ref xref}) 113 | ([xref att] 114 | {::type ::xref ::ref xref ::att att})) 115 | 116 | (defn pseudo [param] 117 | {::type ::pseudo 118 | ::param param}) 119 | 120 | (defn join 121 | [delimiter values] 122 | {::type ::join 123 | ::fn-values values 124 | ::delimiter delimiter}) 125 | 126 | (defn fn-if 127 | [condition-name value-if-true value-if-false] 128 | {::type ::if 129 | ::condition-name condition-name 130 | ::value-if-true value-if-true 131 | ::value-if-false value-if-false}) 132 | 133 | (defn select [index values] 134 | {::type ::select 135 | ::index index 136 | ::fn-values values}) 137 | 138 | (defn equals [x y] 139 | {::type ::equals 140 | ::x x 141 | ::y y}) 142 | 143 | (defn find-in-map [map-name top-level-key second-level-key] 144 | {::type ::find-in-map 145 | ::map-name map-name 146 | ::top-level-key top-level-key 147 | ::second-level-key second-level-key}) 148 | 149 | (s/def ::value-name (spec-or-ref string?)) 150 | (s/def ::import-value (s/keys :req [::value-name])) 151 | 152 | (defmethod value-type ::import-value [_] ::import-value) 153 | 154 | (defn import-value [value-name] 155 | {::type ::import-value 156 | ::value-name value-name}) 157 | 158 | (s/def ::sub-literal (spec-or-ref string?)) 159 | (s/def ::sub (s/keys :req [::sub-literal])) 160 | 161 | (defmethod value-type ::sub [_] ::sub) 162 | 163 | (defn sub [string-to-interpolate] 164 | {::type ::sub 165 | ::sub-literal string-to-interpolate}) 166 | 167 | 168 | (defmethod value-type ::base64 [_] ::base64) 169 | 170 | (s/def ::input-string (spec-or-ref string?)) 171 | (s/def ::base64 (s/keys :req [::input-string])) 172 | 173 | (defn base64 [input-string] 174 | {::type ::base64 175 | ::input-string input-string}) 176 | -------------------------------------------------------------------------------- /test/crucible/aws/ec2_test.clj: -------------------------------------------------------------------------------- 1 | (ns crucible.aws.ec2-test 2 | (:require [clojure.test :refer :all] 3 | [crucible.aws.ec2 :as ec2] 4 | [crucible.resources :as res] 5 | [cheshire.core :as json] 6 | [clojure.spec.alpha :as s] 7 | [crucible.core :refer [template encode parameter xref]])) 8 | 9 | (deftest vpc-test 10 | (testing "minimal spec" 11 | (is (s/valid? ::res/resource (second (ec2/vpc {::ec2/cidr-block "1.2.3.4/24"})))))) 12 | 13 | (deftest igw-test 14 | (testing "minimal spec" 15 | (is (s/valid? ::res/resource (second (ec2/internet-gateway {})))))) 16 | 17 | (deftest nat-gateway-test 18 | (testing "minimal spec" 19 | (is (s/valid? ::res/resource (second (ec2/nat-gateway {::ec2/allocation-id "id" 20 | ::ec2/subnet-id "id"}))))) 21 | (testing "full spec" 22 | (is (s/valid? ::res/resource (second (ec2/nat-gateway {::ec2/allocation-id "id" 23 | ::ec2/subnet-id "id" 24 | ::ec2/tags [{::res/key "key" ::res/value "value"}]}))))) 25 | (testing "template with multiple conditions" 26 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 27 | "Description" "t" 28 | "Resources" {"NatGateway" {"Type" "AWS::EC2::NatGateway" 29 | "Properties" {"AllocationId" "id" 30 | "SubnetId" "id"}}}} 31 | (cheshire.core/decode 32 | (encode 33 | (template "t" 34 | :nat-gateway (ec2/nat-gateway {::ec2/allocation-id "id" 35 | ::ec2/subnet-id "id"})))))))) 36 | 37 | (deftest route-table-test 38 | (testing "minimal spec" 39 | (is (s/valid? ::res/resource (second (ec2/route-table {::ec2/vpc-id "id"}))))) 40 | (testing "full spec" 41 | (is (s/valid? ::res/resource (second (ec2/route-table {::ec2/vpc-id "id" 42 | ::ec2/tags [{::res/key "key" ::res/value "value"}]}))))) 43 | (testing "template with route table" 44 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 45 | "Description" "t" 46 | "Resources" {"RouteTable" {"Type" "AWS::EC2::RouteTable" 47 | "Properties" {"VpcId" "id"}}}} 48 | (cheshire.core/decode 49 | (encode 50 | (template "t" 51 | :route-table (ec2/route-table {::ec2/vpc-id "id"})))))))) 52 | 53 | (deftest sg-test 54 | (testing "encode" 55 | (is (= {"AWSTemplateFormatVersion" "2010-09-09" 56 | "Description" "minimal" 57 | "Resources" 58 | {"MySecurityGroup" 59 | {"Type" "AWS::EC2::SecurityGroup" 60 | "Properties" {"GroupDescription" 61 | "Enable SSH access and HTTP from the load balancer only" 62 | "SecurityGroupIngress" 63 | [{"IpProtocol" "tcp" 64 | "FromPort" 22 65 | "ToPort" 22 66 | "CidrIp" "0.0.0.0/0"} 67 | {"IpProtocol" "tcp" 68 | "FromPort" { "Ref" "WebServerPort" } 69 | "ToPort" { "Ref" "WebServerPort" } 70 | "SourceSecurityGroupOwnerId" {"Fn::GetAtt" 71 | ["ElasticLoadBalancer" 72 | "SourceSecurityGroup.OwnerAlias"]} 73 | "SourceSecurityGroupName" {"Fn::GetAtt" 74 | ["ElasticLoadBalancer" 75 | "SourceSecurityGroup.GroupName"]}}]} 76 | }} 77 | "Parameters" {"ElasticLoadBalancer" {"Type" "String"} 78 | "WebServerPort" {"Type" "String"}}} 79 | (json/decode 80 | (encode 81 | (template 82 | "minimal" 83 | :elastic-load-balancer (parameter) 84 | :web-server-port (parameter) 85 | :my-security-group 86 | (ec2/security-group 87 | {::ec2/group-description "Enable SSH access and HTTP from the load balancer only" 88 | ::ec2/security-group-ingress 89 | [{::ec2/ip-protocol "tcp" 90 | ::ec2/from-port 22 91 | ::ec2/to-port 22 92 | ::ec2/cidr-ip "0.0.0.0/0"} 93 | {::ec2/ip-protocol "tcp" 94 | ::ec2/from-port (xref :web-server-port) 95 | ::ec2/to-port (xref :web-server-port) 96 | ::ec2/source-security-group-owner-id 97 | (xref :elastic-load-balancer (keyword "SourceSecurityGroup.OwnerAlias")) 98 | :source-security-group-name 99 | (xref :elastic-load-balancer (keyword "SourceSecurityGroup.GroupName"))}]})))))))) 100 | 101 | (deftest ebs-volume-test 102 | (testing "test ebs volume spec" 103 | (is (s/valid? ::res/resource 104 | (second (ec2/ebs-volume 105 | {::ec2/availability-zone "us-east-1c" 106 | ::ec2/size 200 107 | ::ec2/volume-type "gp2" 108 | ::res/tags [{::res/key "Name" 109 | ::res/value "My Volume"}]})))))) 110 | --------------------------------------------------------------------------------