From 701b25779ee8338c46a6b0f8afff6eb2e812d3f9 Mon Sep 17 00:00:00 2001 From: Adam Pippin Date: Thu, 18 Feb 2021 11:48:10 -0800 Subject: [PATCH] Allow !replace to accept a scalar This is the default merge behaviour, but simplifies using !replace because passing in a var that happens to evaluate to a scalar won't cause an issue anymore --- app/Cfnpp/Functions.php | 5 +- app/CloudFormation/Client.php | 74 +++++++++++++++++++ app/CloudFormation/Stack.php | 63 ++++++++++++++++ app/Commands/Stack/Deploy.php | 85 ++++++++++++++++++++++ app/Commands/TrashCommand.php | 68 +++++++++++++++++ doc1.yaml | 18 +++++ doc2.yaml | 14 ++++ doc3.yaml | 10 +++ doc4.yaml | 1 + ecs.yaml | 133 ++++++++++++++++++++++++++++++++++ expressions.txt | 41 +++++++++++ idea.txt | 67 +++++++++++++++++ out.yaml | 10 +++ phan.txt | 29 ++++++++ test.yaml | 25 +++++++ test2.yaml | 9 +++ test3.yaml | 20 +++++ tmp.yml | 11 +++ 18 files changed, 682 insertions(+), 1 deletion(-) create mode 100644 app/CloudFormation/Client.php create mode 100644 app/CloudFormation/Stack.php create mode 100644 app/Commands/Stack/Deploy.php create mode 100644 app/Commands/TrashCommand.php create mode 100644 doc1.yaml create mode 100644 doc2.yaml create mode 100644 doc3.yaml create mode 100644 doc4.yaml create mode 100644 ecs.yaml create mode 100644 expressions.txt create mode 100644 idea.txt create mode 100644 out.yaml create mode 100644 phan.txt create mode 100644 test.yaml create mode 100644 test2.yaml create mode 100644 test3.yaml create mode 100644 tmp.yml diff --git a/app/Cfnpp/Functions.php b/app/Cfnpp/Functions.php index 29131a8..6321ce1 100644 --- a/app/Cfnpp/Functions.php +++ b/app/Cfnpp/Functions.php @@ -83,7 +83,10 @@ class Functions */ public function mf_replace(Node $original, Node $target, NodeFunction $function): ?Node { - // TODO: Deal with nodefunctionvalue + if ($function instanceof NodeFunctionValue) + { + return new NodeValue(null, $target->hasName() ? $target->getName() : null, $function->getValue()); + } $replacement = new Node(null, $target->hasName() ? $target->getName() : null); $replacement->setChildren($function->getChildren()); diff --git a/app/CloudFormation/Client.php b/app/CloudFormation/Client.php new file mode 100644 index 0000000..77de129 --- /dev/null +++ b/app/CloudFormation/Client.php @@ -0,0 +1,74 @@ +cfn = new CloudFormationClient([ + 'version' => 'latest', + 'region' => $region ?? env('AWS_REGION'), + 'profile' => $profile ?? env('AWS_PROFILE') + ]); + } + + public function getStack(string $stack): ?Stack + { + try + { + $stack_description = $this->cfn->describeStacks([ + 'StackName' => $stack + ]); + } + catch (\Aws\Exception\AwsException $ex) + { + if ($ex->getAwsErrorMessage() == 'Stack with id '.$stack.' does not exist') + { + return null; + } + throw new \Exception($ex->getAwsErrorMessage(), $ex->getAwsErrorCode()); + } + return new Stack($stack_description['Stacks'][0]); + } + + public function createStack(string $stack_name, string $template_body, array $parameters = []): Stack + { + $response = $this->cfn->createStack($this->buildStackRequest($stack_name, $template_body, $parameters)); + return $this->getStack($response['StackId']); + } + + public function updateStack(string $stack_name, string $template_body, array $parameters = []): Stack + { + $response = $this->cfn->updateStack($this->buildStackRequest($stack_name, $template_body, $parameters)); + return $this->getStack($response['StackId']); + } + + protected function buildStackRequest(string $stack_name, string $template_body, array $parameters = []): array + { + $request = [ + 'StackName' => $stack_name, + 'TemplateBody' => $template_body, + 'Capabilities' => ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], + 'TimeoutInMinutes' => 5 + ]; + if (sizeof($parameters)) + { + $request['Parameters'] = []; + foreach ($parameters as $k => $v) + { + $request['Parameters'][] = [ + 'ParameterKey' => $k, + 'ParameterValue' => $v + ]; + } + } + return $request; + } +} diff --git a/app/CloudFormation/Stack.php b/app/CloudFormation/Stack.php new file mode 100644 index 0000000..1bbde49 --- /dev/null +++ b/app/CloudFormation/Stack.php @@ -0,0 +1,63 @@ +stack_data = $data; + } + + public function getId(): string + { + return $this->stack_data['StackId']; + } + + public function getName(): string + { + return $this->stack_data['StackName']; + } + + public function getStatus(): string + { + return $this->stack_data['StackStatus']; + } + + public function getStatusReason(): ?string + { + return $this->stack_data['StackStatusReason'] ?? null; + } + + public function getParameters(): array + { + if (!isset($this->stack_data['Parameters'])) + { + return []; + } + $parameters = []; + foreach ($this->stack_data['Parameters'] as $parameter) + { + $parameters[$parameter['ParameterKey']] = $parameter['ParameterValue']; + } + return $parameters; + } + + public function getOutputs(): array + { + if (!isset($this->stack_data['Outputs'])) + { + return []; + } + $outputs = []; + foreach ($this->stack_data['Outputs'] as $output) + { + $outputs[$output['OutputKey']] = $output['OutputValue']; + } + return $outputs; + } +} diff --git a/app/Commands/Stack/Deploy.php b/app/Commands/Stack/Deploy.php new file mode 100644 index 0000000..862d921 --- /dev/null +++ b/app/Commands/Stack/Deploy.php @@ -0,0 +1,85 @@ + + */ +class Deploy extends Command +{ + /** + * The signature of the command. + * + * @var string + */ + protected $signature = 'stack:deploy {in_file} {--format=Yaml}'; + + /** + * The description of the command. + * + * @var string + */ + protected $description = 'Read an input file, process, output result to a file'; + + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $engine = new \App\Engine\Engine(); + + $serializer = $this->getSerializer(); + $unserializer = $this->getUnserializer(); + $engine->setSerializer($serializer)->setUnserializer($unserializer)->setCompiler(new \App\Cfnpp\Compiler()); + $options = new \App\Cfnpp\Options(); + + $output = $engine->process($this->argument('in_file'), $options); + + file_put_contents($this->argument('out_file'), $output); + } + + /** + * Retrieve a serializer instance based on the format specified in the command options. + * + * @return \App\Serialize\ISerialize + */ + protected function getSerializer(): \App\Serialize\ISerialize + { + $format = $this->option('format'); + $class = '\\App\\Serialize\\'.$format; + + if (!class_exists($class)) + { + throw new \Exception('Unknown formatter: '.$format); + } + + return new $class(); + } + + /** + * Retrieve a unserializer instance based on the format specified in the command options. + * + * @return \App\Serialize\IUnserialize + */ + protected function getUnserializer(): \App\Serialize\IUnserialize + { + $format = $this->option('format'); + $class = '\\App\\Serialize\\'.$format; + + if (!class_exists($class)) + { + throw new \Exception('Unknown formatter: '.$format); + } + + return new $class(); + } +} diff --git a/app/Commands/TrashCommand.php b/app/Commands/TrashCommand.php new file mode 100644 index 0000000..ef8b87c --- /dev/null +++ b/app/Commands/TrashCommand.php @@ -0,0 +1,68 @@ +setVariables(['A' => true]); + $options->setParameters(['LBType' => 'internal', 'Other' => true]); + + $expr = new \App\Cfnpp\Expression\Expression('"internal" Other eq', $options); + + if ($expr->isComplete()) + { + echo 'Got value: '.PHP_EOL; + var_dump($expr->toArray()); + } + else + { + echo 'Expression not resolved: '.PHP_EOL; + var_dump($expr->toCloudformation()); + } + /* + $cfn = new \App\CloudFormation\Client(); + $stack = $cfn->getStack('CloudBender'); + if (!isset($stack)) + throw new \Exception("Stack not found"); + var_dump($stack->getId(), $stack->getName(), $stack->getStatus(), $stack->getStatusReason(), $stack->getParameters(), $stack->getOutputs()); + */ + } + + /** + * Define the command's schedule. + * + * @param \Illuminate\Console\Scheduling\Schedule $schedule + * @return void + */ + public function schedule(Schedule $schedule): void + { + // $schedule->command(static::class)->everyMinute(); + } +} diff --git a/doc1.yaml b/doc1.yaml new file mode 100644 index 0000000..a3960e7 --- /dev/null +++ b/doc1.yaml @@ -0,0 +1,18 @@ +cfnpp: + stack: + - doc3.yaml + - doc4.yaml + variables: + A: 2 + B: 2 + +AwsThing: '2020-01-01' + +Resources: + Thing: + A: 1 + B: 2 + AnArr: + - 1 + - 2 + Thing3: !var C diff --git a/doc2.yaml b/doc2.yaml new file mode 100644 index 0000000..c3e8f4b --- /dev/null +++ b/doc2.yaml @@ -0,0 +1,14 @@ +cfnpp: + stack: + - doc1.yaml + variables: + A: 1 + parameters: + MyParam: asdf + +Parameters: + MyParam: + Type: String + Default: '' + Description: 'My parameter' + diff --git a/doc3.yaml b/doc3.yaml new file mode 100644 index 0000000..d52b42f --- /dev/null +++ b/doc3.yaml @@ -0,0 +1,10 @@ +cfnpp: + stack: + - doc4.yaml + variables: + C: 3 + D: 3 + +Resources: + NewResource: + Type: AWS::Custom diff --git a/doc4.yaml b/doc4.yaml new file mode 100644 index 0000000..d40b518 --- /dev/null +++ b/doc4.yaml @@ -0,0 +1 @@ +Resources: 1 diff --git a/ecs.yaml b/ecs.yaml new file mode 100644 index 0000000..ff8fb54 --- /dev/null +++ b/ecs.yaml @@ -0,0 +1,133 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Deploy a service into an ECS cluster behind a private load balancer. +Parameters: + StackName: + Type: String + Default: production + Description: The name of the parent cluster stack that you created. Necessary + to locate and reference resources created by that stack. + ServiceName: + Type: String + Default: nginx + Description: A name for the service + ImageUrl: + Type: String + Default: nginx + Description: The url of a docker image that contains the application process that + will handle the traffic for this service + ContainerPort: + Type: Number + Default: 80 + Description: What port number the application inside the docker container is binding to + ContainerCpu: + Type: Number + Default: 256 + Description: How much CPU to give the container. 1024 is 1 CPU + ContainerMemory: + Type: Number + Default: 512 + Description: How much memory in megabytes to give the container + Path: + Type: String + Default: "*" + Description: A path on the public load balancer that this service + should be connected to. Use * to send all load balancer + traffic to this service. + Priority: + Type: Number + Default: 1 + Description: The priority for the routing rule added to the load balancer. + This only applies if your have multiple services which have been + assigned to different paths on the load balancer. + DesiredCount: + Type: Number + Default: 2 + Description: How many copies of the service task to run + Role: + Type: String + Default: "" + Description: (Optional) An IAM role to give the service's containers if the code within needs to + access other AWS resources like S3 buckets, DynamoDB tables, etc + +Conditions: + HasCustomRole: !Not [ !Equals [!Ref 'Role', ''] ] + +Resources: + + # The task definition. This is a simple metadata description of what + # container to run, and what resource requirements it has. + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Ref 'ServiceName' + Cpu: !Ref 'ContainerCpu' + Memory: !Ref 'ContainerMemory' + TaskRoleArn: + Fn::If: + - 'HasCustomRole' + - !Ref 'Role' + - !Ref "AWS::NoValue" + ContainerDefinitions: + - Name: !Ref 'ServiceName' + Cpu: !Ref 'ContainerCpu' + Memory: !Ref 'ContainerMemory' + Image: !Ref 'ImageUrl' + PortMappings: + - ContainerPort: !Ref 'ContainerPort' + + # The service. The service is a resource which allows you to run multiple + # copies of a type of task, and gather up their logs and metrics, as well + # as monitor the number of running tasks and replace any that have crashed + Service: + Type: AWS::ECS::Service + DependsOn: LoadBalancerRule + Properties: + ServiceName: !Ref 'ServiceName' + Cluster: + Fn::ImportValue: + !Join [':', [!Ref 'StackName', 'ClusterName']] + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 75 + DesiredCount: !Ref 'DesiredCount' + TaskDefinition: !Ref 'TaskDefinition' + LoadBalancers: + - ContainerName: !Ref 'ServiceName' + ContainerPort: !Ref 'ContainerPort' + TargetGroupArn: !Ref 'TargetGroup' + + # A target group. This is used for keeping track of all the tasks, and + # what IP addresses / port numbers they have. You can query it yourself, + # to use the addresses yourself, but most often this target group is just + # connected to an application load balancer, or network load balancer, so + # it can automatically distribute traffic across all the targets. + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + HealthCheckIntervalSeconds: 6 + HealthCheckPath: / + HealthCheckProtocol: HTTP + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 2 + Name: !Ref 'ServiceName' + Port: 80 + Protocol: HTTP + UnhealthyThresholdCount: 2 + VpcId: + Fn::ImportValue: + !Join [':', [!Ref 'StackName', 'VPCId']] + + # Create a rule on the load balancer for routing traffic to the target group + LoadBalancerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - TargetGroupArn: !Ref 'TargetGroup' + Type: 'forward' + Conditions: + - Field: path-pattern + Values: [!Ref 'Path'] + ListenerArn: + Fn::ImportValue: + !Join [':', [!Ref 'StackName', 'PrivateListener']] + Priority: !Ref 'Priority' diff --git a/expressions.txt b/expressions.txt new file mode 100644 index 0000000..1eca09f --- /dev/null +++ b/expressions.txt @@ -0,0 +1,41 @@ +- naked variable names +- single quoted strings +- numbers are numbers +- postfix? +- english-y operators? + +LoadBalancerType 'internet-facing' eq LoadBalancerType defined or + +Maybe function calls for some of the kind of stuff we need in expressions? + +'LoadBalancerType' parameter 'internet-facing' eq + +How do we differentiate between parameters and variables? Do we shove those all in one bucket? + +Should statements that reference parameters simply be evaluated as far as we can, then the +remainder converted into a cfn condition+fn::if? + + +Pass 1: + - Load all files + - Build dependency graph + +Pass 2: + - Run through files in dependency graph + - Run mergeFunctions + merge for each + +Pass 3: + - Load all variables (does this need a dependency graph as well?) -- can grab these from the final document + - Load all parameters (again, do we need a dependency graph?) -- can grab these from the final document + - Evaluate variables + parameters + +Pass 3: + - Run other functions (e.g., !var, !if, etc) + +So maybe pass 1: + mergeFunctions + merge + +Pass 2: + + diff --git a/idea.txt b/idea.txt new file mode 100644 index 0000000..630b41e --- /dev/null +++ b/idea.txt @@ -0,0 +1,67 @@ +Okay so... + +Take document 1 + document 2 +Do a recursive merge + - Named properties overwrite scalars + - This is based on the _target_ type, if the source has a map and the target has a scalar... it overwrites the entire map with the scalar, + if the target is a map and the source is a scalar, the scalar is tossed away and overwritten + +Later, implement ways to control this merge. E.g., "!Replace" to replace an entire block. + +Things we might need: + !Replace -- replace entire block + !Unset -- remove value, any children, etc + + + +So: +``` +AwsWhatever: '2020-01-01' +Resources: + SecurityGroup: + VpcId: 1234 + Name: Test + UseSuperSecurity: True + Subnets: + - us-east-1a + - us-east-1b + Users: + Alice: 'aws:arn:1234:alice' + Bob: 'aws:arn:1234:bob' +``` ++ +``` +Resources: + SecurityGroup: + Name: NewGroup + UseSuperSecurity: !Unset ~ + NewValue: True + Subnets: + - us-east-1c + Users: !Replace + Charlie: 'aws:arn:1234:charlie' +``` += +``` +AwsWhatever: '2020-01-01' +Resources: + SecurityGroup: + # Values not specified are unchanged + VpcId: 1234 + # Scalar values specified are replaced + Name: NewGroup + # !Unset removes a value/block/etc + # UserSuperSecurity: + # Values that don't exist in the original doc are added + NewValue: True + # Arrays are appended + Subnets: + - us-east-1a + - us-east-1b + - us-east-1c + # Replace stops the merge and just does a copy from the new document + Users: + Charlie: 'aws:arn:1234:charlie' +``` + + diff --git a/out.yaml b/out.yaml new file mode 100644 index 0000000..56e2349 --- /dev/null +++ b/out.yaml @@ -0,0 +1,10 @@ +cfnpp: + parameters: + CreateDnsRecord: whatever + variables: + A: true +Resources: + Asdf: False! +Metadata: + Stack: + - '' diff --git a/phan.txt b/phan.txt new file mode 100644 index 0000000..8f2ad34 --- /dev/null +++ b/phan.txt @@ -0,0 +1,29 @@ +app/CloudFormation/Client.php:6 PhanPluginNoCommentOnClass Class \App\CloudFormation\Client has no doc comment +app/CloudFormation/Client.php:8 PhanPluginNoCommentOnProtectedProperty Protected property \App\CloudFormation\Client->cfn has no doc comment +app/CloudFormation/Client.php:8 PhanPluginUnknownPropertyType Property \App\CloudFormation\Client->cfn has an initial type that cannot be inferred (Types inferred after analysis: \Aws\CloudFormation\CloudFormationClient) +app/CloudFormation/Client.php:20 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Client::getStack has no doc comment +app/CloudFormation/Client.php:34 PhanTypeMismatchArgumentInternal Argument 2 ($code) is $ex->getAwsErrorCode() of type null|string but \Exception::__construct() takes int +app/CloudFormation/Client.php:39 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Client::createStack has no doc comment +app/CloudFormation/Client.php:39 PhanPluginUnknownArrayMethodParamType Method \App\CloudFormation\Client::createStack has a parameter type of array for $parameters, but does not specify any key types or value types +app/CloudFormation/Client.php:42 PhanTypeMismatchReturnNullable Returning $this->getStack($response['StackId']) of type ?\App\CloudFormation\Stack but createStack() is declared to return \App\CloudFormation\Stack (expected returned value to be non-nullable) +app/CloudFormation/Client.php:45 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Client::updateStack has no doc comment +app/CloudFormation/Client.php:45 PhanPluginUnknownArrayMethodParamType Method \App\CloudFormation\Client::updateStack has a parameter type of array for $parameters, but does not specify any key types or value types +app/CloudFormation/Client.php:48 PhanTypeMismatchReturnNullable Returning $this->getStack($response['StackId']) of type ?\App\CloudFormation\Stack but updateStack() is declared to return \App\CloudFormation\Stack (expected returned value to be non-nullable) +app/CloudFormation/Client.php:52 PhanPluginNoCommentOnProtectedMethod Protected method \App\CloudFormation\Client::buildStackRequest has no doc comment +app/CloudFormation/Client.php:52 PhanPluginUnknownArrayMethodParamType Method \App\CloudFormation\Client::buildStackRequest has a parameter type of array for $parameters, but does not specify any key types or value types +app/CloudFormation/Client.php:52 PhanPluginUnknownArrayMethodReturnType Method \App\CloudFormation\Client::buildStackRequest() has a return type of array, but does not specify any key types or value types +app/CloudFormation/Stack.php:5 PhanPluginNoCommentOnClass Class \App\CloudFormation\Stack has no doc comment +app/CloudFormation/Stack.php:7 PhanPluginNoCommentOnProtectedProperty Protected property \App\CloudFormation\Stack->stack_data has no doc comment +app/CloudFormation/Stack.php:7 PhanPluginUnknownPropertyType Property \App\CloudFormation\Stack->stack_data has an initial type that cannot be inferred (Types inferred after analysis: array|array) +app/CloudFormation/Stack.php:9 PhanPluginUnknownArrayMethodParamType Method \App\CloudFormation\Stack::__construct has a parameter type of array for $data, but does not specify any key types or value types +app/CloudFormation/Stack.php:14 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Stack::getId has no doc comment +app/CloudFormation/Stack.php:19 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Stack::getName has no doc comment +app/CloudFormation/Stack.php:24 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Stack::getStatus has no doc comment +app/CloudFormation/Stack.php:29 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Stack::getStatusReason has no doc comment +app/CloudFormation/Stack.php:34 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Stack::getParameters has no doc comment +app/CloudFormation/Stack.php:34 PhanPluginUnknownArrayMethodReturnType Method \App\CloudFormation\Stack::getParameters() has a return type of array, but does not specify any key types or value types +app/CloudFormation/Stack.php:45 PhanPluginNoCommentOnPublicMethod Public method \App\CloudFormation\Stack::getOutputs has no doc comment +app/CloudFormation/Stack.php:45 PhanPluginUnknownArrayMethodReturnType Method \App\CloudFormation\Stack::getOutputs() has a return type of array, but does not specify any key types or value types +app/Commands/TrashCommand.php:8 PhanPluginNoCommentOnClass Class \App\Commands\TrashCommand has no doc comment +app/Commands/TrashCommand.php:29 PhanPluginAlwaysReturnMethod Method \App\Commands\TrashCommand::handle has a return type of mixed, but may fail to return a value +app/Commands/TrashCommand.php:29 PhanTypeMissingReturn Method \App\Commands\TrashCommand::handle is declared to return mixed in phpdoc but has no return value diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000..1cfb0de --- /dev/null +++ b/test.yaml @@ -0,0 +1,25 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: A sample template +Resources: + MyEC2Instance: #An inline comment + Type: "AWS::EC2::Instance" + Properties: + ImageId: "ami-0ff8a91507f77f867" #Another comment -- This is a Linux AMI + InstanceType: t2.micro + KeyName: testkey + BlockDeviceMappings: + - + DeviceName: /dev/sdm + Ebs: + VolumeType: io1 + Iops: 200 + DeleteOnTermination: false + VolumeSize: 20 + TestKey: !Replace asdf + OtherKey: !Replace + Test: 1 + Foo: "bar" + Nest: + - 1 + - 2 + - 3 diff --git a/test2.yaml b/test2.yaml new file mode 100644 index 0000000..91f9a3e --- /dev/null +++ b/test2.yaml @@ -0,0 +1,9 @@ +Resources: + Thing1: + Asdf: !Sub "test" + Foo: !Unset ~ + Fdsa: !Replace + - foo + - bar + - baz + diff --git a/test3.yaml b/test3.yaml new file mode 100644 index 0000000..6a2b0b2 --- /dev/null +++ b/test3.yaml @@ -0,0 +1,20 @@ +cfnpp: + parameters: + MultiAz: True + Param1: !var UseSecurity + variables: + UseSecurity: False + UseMultiAz: False + +Resources: + MyResource: + Name: A Resource + AvailabilityZones: !if + - MultiAz Param1 and + - + - us-east-1a + - us-east-1b + - + - us-east-1a + Test: !param Param1 + #Test2: !expr UseSecurity not diff --git a/tmp.yml b/tmp.yml new file mode 100644 index 0000000..e407c3a --- /dev/null +++ b/tmp.yml @@ -0,0 +1,11 @@ +cfnpp: + parameters: + CreateDnsRecord: whatever + variables: + A: true + +Resources: + Asdf: !if + - CreateDnsRecord 1 and A or not + - True! + - False!