awslabs.eks-mcp-server 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/Dockerfile +2 -2
  2. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/PKG-INFO +29 -24
  3. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/README.md +28 -23
  4. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/aws_helper.py +3 -2
  5. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/eks_stack_handler.py +21 -1
  6. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/k8s_apis.py +12 -8
  7. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/k8s_handler.py +35 -0
  8. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/pyproject.toml +1 -1
  9. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_aws_helper.py +5 -3
  10. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_eks_stack_handler.py +80 -0
  11. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_k8s_apis.py +60 -60
  12. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_k8s_handler.py +179 -65
  13. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/.gitignore +0 -0
  14. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/.pre-commit-config.yaml +0 -0
  15. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/.python-version +0 -0
  16. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/CHANGELOG.md +0 -0
  17. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/LICENSE +0 -0
  18. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/NOTICE +0 -0
  19. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/__init__.py +0 -0
  20. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/__init__.py +0 -0
  21. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/cloudwatch_handler.py +0 -0
  22. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/consts.py +0 -0
  23. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/eks_kb_handler.py +0 -0
  24. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/iam_handler.py +0 -0
  25. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/k8s_client_cache.py +0 -0
  26. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/logging_helper.py +0 -0
  27. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/models.py +0 -0
  28. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/server.py +0 -0
  29. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/templates/eks-templates/eks-with-vpc.yaml +0 -0
  30. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/templates/k8s-templates/deployment.yaml +0 -0
  31. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/awslabs/eks_mcp_server/templates/k8s-templates/service.yaml +0 -0
  32. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/docker-healthcheck.sh +0 -0
  33. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_cloudwatch_handler.py +0 -0
  34. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_eks_kb_handler.py +0 -0
  35. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_iam_handler.py +0 -0
  36. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_init.py +0 -0
  37. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_k8s_client_cache.py +0 -0
  38. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_logging_helper.py +0 -0
  39. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_main.py +0 -0
  40. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_models.py +0 -0
  41. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/tests/test_server.py +0 -0
  42. {awslabs_eks_mcp_server-0.1.1 → awslabs_eks_mcp_server-0.1.2}/uv.lock +0 -0
@@ -9,7 +9,7 @@
9
9
  # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
10
  # and limitations under the License.
11
11
 
12
- FROM public.ecr.aws/sam/build-python3.10@sha256:a40f492a0cd8d76557f8a187fc00e49e8864b3cea683e74718ce317790c1ce61 AS uv
12
+ FROM public.ecr.aws/sam/build-python3.10@sha256:e78695db10ca8cb129e59e30f7dc9789b0dbd0181dba195d68419c72bac51ac1 AS uv
13
13
 
14
14
  # Install the project into `/app`
15
15
  WORKDIR /app
@@ -43,7 +43,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
43
43
  # Make the directory just in case it doesn't exist
44
44
  RUN mkdir -p /root/.local
45
45
 
46
- FROM public.ecr.aws/sam/build-python3.10@sha256:a40f492a0cd8d76557f8a187fc00e49e8864b3cea683e74718ce317790c1ce61
46
+ FROM public.ecr.aws/sam/build-python3.10@sha256:e78695db10ca8cb129e59e30f7dc9789b0dbd0181dba195d68419c72bac51ac1
47
47
 
48
48
  # Place executables in the environment at the front of the path and include other binaries
49
49
  ENV PATH="/app/.venv/bin:$PATH:/usr/sbin"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.eks-mcp-server
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: An AWS Labs Model Context Protocol (MCP) server for EKS
5
5
  Project-URL: homepage, https://awslabs.github.io/mcp/
6
6
  Project-URL: docs, https://awslabs.github.io/mcp/servers/eks-mcp-server/
@@ -89,30 +89,35 @@ For read operations, the following permissions are required:
89
89
 
90
90
  ### Write Operations Policy
91
91
 
92
- For write operations, the following permissions are required:
93
-
94
- ```
95
- {
96
- "Version": "2012-10-17",
97
- "Statement": [
98
- {
99
- "Effect": "Allow",
100
- "Action": [
101
- "cloudformation:CreateStack",
102
- "cloudformation:UpdateStack",
103
- "cloudformation:DeleteStack",
104
- "iam:PutRolePolicy"
105
- ],
106
- "Resource": "*",
107
- "Condition": {
108
- "StringEquals": {
109
- "aws:RequestTag/CreatedBy": "EksMcpServer"
110
- }
92
+ For write operations, we recommend the following IAM policies to ensure successful deployment of EKS clusters using the CloudFormation template in `/awslabs/eks_mcp_server/templates/eks-templates/eks-with-vpc.yaml`:
93
+ - [**IAMFullAccess**](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/IAMFullAccess.html): Enables creation and management of IAM roles and policies required for cluster operation
94
+ - [**AmazonVPCFullAccess**](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonVPCFullAccess.html): Allows creation and configuration of VPC resources including subnets, route tables, internet gateways, and NAT gateways
95
+ - [**AWSCloudFormationFullAccess**](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSCloudFormationFullAccess.html): Provides permissions to create, update, and delete CloudFormation stacks that orchestrate the deployment
96
+ - **EKS Full Access (provided below)**: Required for creating and managing EKS clusters, including control plane configuration, node groups, and add-ons
97
+ ```
98
+ {
99
+ "Version": "2012-10-17",
100
+ "Statement": [
101
+ {
102
+ "Effect": "Allow",
103
+ "Action": "eks:*",
104
+ "Resource": "*"
111
105
  }
112
- }
113
- ]
114
- }
115
- ```
106
+ ]
107
+ }
108
+ ```
109
+
110
+
111
+ **Important Security Note**: Users should exercise caution when `--allow-write` and `--allow-sensitive-data-access` modes are enabled with these broad permissions, as this combination grants significant privileges to the MCP server. Only enable these flags when necessary and in trusted environments. For production use, consider creating more restrictive custom policies.
112
+
113
+ ### Kubernetes API Access Requirements
114
+
115
+ All Kubernetes API operations will only work when one of the following conditions is met:
116
+
117
+ 1. The user's principal (IAM role/user) actually created the EKS cluster being accessed
118
+ 2. An EKS Access Entry has been configured for the user's principal
119
+
120
+ If you encounter authorization errors when using Kubernetes API operations, verify that an access entry has been properly configured for your principal.
116
121
 
117
122
  ## Quickstart
118
123
 
@@ -55,30 +55,35 @@ For read operations, the following permissions are required:
55
55
 
56
56
  ### Write Operations Policy
57
57
 
58
- For write operations, the following permissions are required:
59
-
60
- ```
61
- {
62
- "Version": "2012-10-17",
63
- "Statement": [
64
- {
65
- "Effect": "Allow",
66
- "Action": [
67
- "cloudformation:CreateStack",
68
- "cloudformation:UpdateStack",
69
- "cloudformation:DeleteStack",
70
- "iam:PutRolePolicy"
71
- ],
72
- "Resource": "*",
73
- "Condition": {
74
- "StringEquals": {
75
- "aws:RequestTag/CreatedBy": "EksMcpServer"
76
- }
58
+ For write operations, we recommend the following IAM policies to ensure successful deployment of EKS clusters using the CloudFormation template in `/awslabs/eks_mcp_server/templates/eks-templates/eks-with-vpc.yaml`:
59
+ - [**IAMFullAccess**](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/IAMFullAccess.html): Enables creation and management of IAM roles and policies required for cluster operation
60
+ - [**AmazonVPCFullAccess**](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonVPCFullAccess.html): Allows creation and configuration of VPC resources including subnets, route tables, internet gateways, and NAT gateways
61
+ - [**AWSCloudFormationFullAccess**](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSCloudFormationFullAccess.html): Provides permissions to create, update, and delete CloudFormation stacks that orchestrate the deployment
62
+ - **EKS Full Access (provided below)**: Required for creating and managing EKS clusters, including control plane configuration, node groups, and add-ons
63
+ ```
64
+ {
65
+ "Version": "2012-10-17",
66
+ "Statement": [
67
+ {
68
+ "Effect": "Allow",
69
+ "Action": "eks:*",
70
+ "Resource": "*"
77
71
  }
78
- }
79
- ]
80
- }
81
- ```
72
+ ]
73
+ }
74
+ ```
75
+
76
+
77
+ **Important Security Note**: Users should exercise caution when `--allow-write` and `--allow-sensitive-data-access` modes are enabled with these broad permissions, as this combination grants significant privileges to the MCP server. Only enable these flags when necessary and in trusted environments. For production use, consider creating more restrictive custom policies.
78
+
79
+ ### Kubernetes API Access Requirements
80
+
81
+ All Kubernetes API operations will only work when one of the following conditions is met:
82
+
83
+ 1. The user's principal (IAM role/user) actually created the EKS cluster being accessed
84
+ 2. An EKS Access Entry has been configured for the user's principal
85
+
86
+ If you encounter authorization errors when using Kubernetes API operations, verify that an access entry has been properly configured for your principal.
82
87
 
83
88
  ## Quickstart
84
89
 
@@ -13,6 +13,7 @@
13
13
 
14
14
  import boto3
15
15
  import os
16
+ from awslabs.eks_mcp_server import __version__
16
17
  from botocore.config import Config
17
18
  from typing import Any, Optional
18
19
 
@@ -38,7 +39,7 @@ class AwsHelper:
38
39
  def create_boto3_client(cls, service_name: str, region_name: Optional[str] = None) -> Any:
39
40
  """Create a boto3 client with the appropriate profile and region.
40
41
 
41
- The client is configured with a custom user agent suffix 'awslabs/mcp/eks-mcp-server/0.1.0'
42
+ The client is configured with a custom user agent suffix 'awslabs/mcp/eks-mcp-server/{version}'
42
43
  to identify API calls made by the EKS MCP Server.
43
44
 
44
45
  Args:
@@ -55,7 +56,7 @@ class AwsHelper:
55
56
  profile = cls.get_aws_profile()
56
57
 
57
58
  # Create config with user agent suffix
58
- config = Config(user_agent_extra='awslabs/mcp/eks-mcp-server/0.1.0')
59
+ config = Config(user_agent_extra=f'awslabs/mcp/eks-mcp-server/{__version__}')
59
60
 
60
61
  # Create session with profile if specified
61
62
  if profile:
@@ -36,7 +36,7 @@ from awslabs.eks_mcp_server.models import (
36
36
  from mcp.server.fastmcp import Context
37
37
  from mcp.types import EmbeddedResource, ImageContent, TextContent
38
38
  from pydantic import Field
39
- from typing import Dict, List, Optional, Tuple, Union, cast
39
+ from typing import Any, Dict, List, Optional, Tuple, Union, cast
40
40
 
41
41
 
42
42
  class EksStackHandler:
@@ -345,6 +345,10 @@ class EksStackHandler:
345
345
  if 'Parameters' in template_yaml and 'ClusterName' in template_yaml['Parameters']:
346
346
  template_yaml['Parameters']['ClusterName']['Default'] = cluster_name
347
347
 
348
+ # Remove checkov metadata from the EKS cluster resource
349
+ if 'Resources' in template_yaml and 'EksCluster' in template_yaml['Resources']:
350
+ self._remove_checkov_metadata(template_yaml['Resources']['EksCluster'])
351
+
348
352
  # Convert back to YAML
349
353
  modified_template = yaml.dump(template_yaml, default_flow_style=False)
350
354
 
@@ -589,6 +593,22 @@ class EksStackHandler:
589
593
  outputs={},
590
594
  )
591
595
 
596
+ def _remove_checkov_metadata(self, resource: Dict[str, Any]) -> None:
597
+ """Remove checkov metadata from a resource and clean up empty Metadata sections.
598
+
599
+ Args:
600
+ resource: The resource dictionary to process
601
+ """
602
+ if 'Metadata' in resource:
603
+ # Check if there's checkov metadata
604
+ if 'checkov' in resource['Metadata']:
605
+ # Remove only the checkov metadata
606
+ del resource['Metadata']['checkov']
607
+
608
+ # If Metadata is now empty, remove it entirely
609
+ if not resource['Metadata']:
610
+ del resource['Metadata']
611
+
592
612
  async def _delete_stack(
593
613
  self, ctx: Context, stack_name: str, cluster_name: str
594
614
  ) -> DeleteStackResponse:
@@ -407,22 +407,26 @@ class K8sApis:
407
407
  Pod logs as a string
408
408
  """
409
409
  try:
410
- # Get the Pod resource using the dynamic client
411
- pod_resource = self.dynamic_client.resources.get(api_version='v1', kind='Pod')
410
+ from kubernetes import client
411
+
412
+ # Create CoreV1Api client
413
+ core_v1_api = client.CoreV1Api(self.api_client)
412
414
 
413
- # Prepare parameters for the log subresource
415
+ # Prepare parameters for the read_namespaced_pod_log method
414
416
  params = {}
415
417
  if container_name:
416
418
  params['container'] = container_name
417
419
  if since_seconds:
418
- params['sinceSeconds'] = since_seconds
420
+ params['since_seconds'] = since_seconds
419
421
  if tail_lines:
420
- params['tailLines'] = tail_lines
422
+ params['tail_lines'] = tail_lines
421
423
  if limit_bytes:
422
- params['limitBytes'] = limit_bytes
424
+ params['limit_bytes'] = limit_bytes
423
425
 
424
- # Call the log subresource (note: singular 'log', not 'logs')
425
- logs_response = pod_resource.log.get(name=pod_name, namespace=namespace, **params)
426
+ # Call the read_namespaced_pod_log method
427
+ logs_response = core_v1_api.read_namespaced_pod_log(
428
+ name=pod_name, namespace=namespace, **params
429
+ )
426
430
 
427
431
  return logs_response
428
432
 
@@ -780,6 +780,37 @@ class K8sHandler:
780
780
  output_file_path='',
781
781
  )
782
782
 
783
+ def _remove_checkov_skip_annotations(self, content: str) -> str:
784
+ """Remove checkov skip annotations from YAML content.
785
+
786
+ Args:
787
+ content: YAML content as string
788
+
789
+ Returns:
790
+ YAML content with checkov skip annotations removed
791
+ """
792
+ # Use yaml to parse and modify the content
793
+ yaml_content = yaml.safe_load(content)
794
+ if (
795
+ yaml_content
796
+ and 'metadata' in yaml_content
797
+ and 'annotations' in yaml_content['metadata']
798
+ ):
799
+ # Remove all checkov skip annotations
800
+ annotations = yaml_content['metadata']['annotations']
801
+ checkov_keys = [key for key in annotations.keys() if key.startswith('checkov.io/skip')]
802
+ for key in checkov_keys:
803
+ del annotations[key]
804
+
805
+ # If annotations is now empty, remove it
806
+ if not annotations:
807
+ del yaml_content['metadata']['annotations']
808
+
809
+ # Convert back to YAML string
810
+ content = yaml.dump(yaml_content, default_flow_style=False)
811
+
812
+ return content
813
+
783
814
  def _load_yaml_template(self, template_files: list, values: Dict[str, Any]) -> str:
784
815
  """Load and process Kubernetes template files.
785
816
 
@@ -804,6 +835,10 @@ class K8sHandler:
804
835
  for key, value in values.items():
805
836
  content = content.replace(key, value)
806
837
 
838
+ # Remove checkov skip annotations if present
839
+ if template_file == 'deployment.yaml':
840
+ content = self._remove_checkov_skip_annotations(content)
841
+
807
842
  template_contents.append(content)
808
843
 
809
844
  # Combine templates into a single YAML document with separator
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "awslabs.eks-mcp-server"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "An AWS Labs Model Context Protocol (MCP) server for EKS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -12,6 +12,7 @@
12
12
  """Tests for the AWS Helper."""
13
13
 
14
14
  import os
15
+ from awslabs.eks_mcp_server import __version__
15
16
  from awslabs.eks_mcp_server.aws_helper import AwsHelper
16
17
  from unittest.mock import ANY, MagicMock, patch
17
18
 
@@ -131,7 +132,7 @@ class TestAwsHelper:
131
132
  )
132
133
 
133
134
  def test_create_boto3_client_user_agent(self):
134
- """Test that create_boto3_client sets the user agent suffix correctly."""
135
+ """Test that create_boto3_client sets the user agent suffix correctly using the package version."""
135
136
  # Create a real Config object to inspect
136
137
  with patch.object(AwsHelper, 'get_aws_profile', return_value=None):
137
138
  with patch.object(AwsHelper, 'get_aws_region', return_value=None):
@@ -143,6 +144,7 @@ class TestAwsHelper:
143
144
  _, kwargs = mock_client.call_args
144
145
  config = kwargs.get('config')
145
146
 
146
- # Verify the user agent suffix
147
+ # Verify the user agent suffix uses the version from __init__.py
147
148
  assert config is not None
148
- assert config.user_agent_extra == 'awslabs/mcp/eks-mcp-server/0.1.0'
149
+ expected_user_agent = f'awslabs/mcp/eks-mcp-server/{__version__}'
150
+ assert config.user_agent_extra == expected_user_agent
@@ -505,6 +505,16 @@ class TestEksStackHandler:
505
505
  ClusterName:
506
506
  Type: String
507
507
  Default: my-cluster
508
+ Resources:
509
+ EksCluster:
510
+ Type: AWS::EKS::Cluster
511
+ Metadata:
512
+ checkov:
513
+ skip:
514
+ - id: CKV_AWS_58
515
+ - comment: "Secrets encryption is enabled by default in EKS 1.27+"
516
+ Properties:
517
+ Name: my-cluster
508
518
  """
509
519
  mock_yaml_content = yaml.safe_load(mock_template_content)
510
520
 
@@ -531,6 +541,76 @@ class TestEksStackHandler:
531
541
  assert result.content[0].type == 'text'
532
542
  assert 'template generated' in result.content[0].text
533
543
 
544
+ # Verify that the Metadata section was removed from the EksCluster resource
545
+ # because it only contained checkov metadata which was removed
546
+ assert 'Resources' in mock_yaml_content
547
+ assert 'EksCluster' in mock_yaml_content['Resources']
548
+ assert 'Metadata' not in mock_yaml_content['Resources']['EksCluster']
549
+
550
+ @pytest.mark.asyncio
551
+ async def test_generate_template_with_other_metadata(self):
552
+ """Test that _generate_template only removes checkov metadata and keeps other metadata."""
553
+ # Create a mock MCP server
554
+ mock_mcp = MagicMock()
555
+
556
+ # Initialize the EKS handler with the mock MCP server
557
+ handler = EksStackHandler(mock_mcp)
558
+
559
+ # Create a mock context
560
+ mock_ctx = MagicMock(spec=Context)
561
+
562
+ # Mock the open function to return a mock file with both checkov and other metadata
563
+ mock_template_content = """
564
+ Parameters:
565
+ ClusterName:
566
+ Type: String
567
+ Default: my-cluster
568
+ Resources:
569
+ EksCluster:
570
+ Type: AWS::EKS::Cluster
571
+ Metadata:
572
+ checkov:
573
+ skip:
574
+ - id: CKV_AWS_58
575
+ - comment: "Secrets encryption is enabled by default in EKS 1.27+"
576
+ other_metadata:
577
+ key: value
578
+ Properties:
579
+ Name: my-cluster
580
+ """
581
+ # Create a deep copy of the YAML content that we can modify
582
+ mock_yaml_content = yaml.safe_load(mock_template_content)
583
+
584
+ # Mock the necessary functions
585
+ with (
586
+ patch('builtins.open', mock_open(read_data=mock_template_content)),
587
+ patch('os.path.dirname', return_value='/mock/path'),
588
+ patch('os.path.join', return_value='/mock/path/template.yaml'),
589
+ patch('os.makedirs', return_value=None),
590
+ patch('yaml.safe_load', return_value=mock_yaml_content),
591
+ patch('yaml.dump', return_value=mock_template_content),
592
+ ):
593
+ # Call the _generate_template method
594
+ result = await handler._generate_template(
595
+ ctx=mock_ctx,
596
+ template_path='/path/to/output/template.yaml',
597
+ cluster_name='test-cluster',
598
+ )
599
+
600
+ # Verify the result
601
+ assert not result.isError
602
+ assert result.template_path == '/path/to/output/template.yaml'
603
+
604
+ # Verify that only the checkov metadata was removed
605
+ assert 'Resources' in mock_yaml_content
606
+ assert 'EksCluster' in mock_yaml_content['Resources']
607
+ assert 'Metadata' in mock_yaml_content['Resources']['EksCluster']
608
+ assert 'checkov' not in mock_yaml_content['Resources']['EksCluster']['Metadata']
609
+ assert 'other_metadata' in mock_yaml_content['Resources']['EksCluster']['Metadata']
610
+ assert mock_yaml_content['Resources']['EksCluster']['Metadata']['other_metadata'] == {
611
+ 'key': 'value'
612
+ }
613
+
534
614
  @pytest.mark.asyncio
535
615
  async def test_manage_eks_stacks_generate(self):
536
616
  """Test that manage_eks_stacks handles the generate operation correctly."""
@@ -419,69 +419,69 @@ class TestK8sApisOperations:
419
419
 
420
420
  def test_get_pod_logs(self, k8s_apis):
421
421
  """Test get_pod_logs method."""
422
- # Mock the dynamic client and resources
423
- mock_resource = MagicMock()
424
- mock_log = MagicMock()
425
- mock_resource.log = mock_log
426
- mock_resources = MagicMock()
427
- mock_resources.get.return_value = mock_resource
428
- k8s_apis.dynamic_client.resources = mock_resources
429
-
430
- # Mock log.get to return logs
431
- mock_log.get.return_value = 'log line 1\nlog line 2\n'
432
-
433
- # Get pod logs with all parameters
434
- logs = k8s_apis.get_pod_logs(
435
- pod_name='test-pod',
436
- namespace='test-namespace',
437
- container_name='test-container',
438
- since_seconds=60,
439
- tail_lines=100,
440
- limit_bytes=1024,
441
- )
442
-
443
- # Verify the result
444
- assert logs == 'log line 1\nlog line 2\n'
445
-
446
- # Verify the dynamic client was used correctly
447
- mock_resources.get.assert_called_once_with(api_version='v1', kind='Pod')
448
- mock_log.get.assert_called_once_with(
449
- name='test-pod',
450
- namespace='test-namespace',
451
- container='test-container',
452
- sinceSeconds=60,
453
- tailLines=100,
454
- limitBytes=1024,
455
- )
422
+ # Mock the CoreV1Api client
423
+ with patch('kubernetes.client') as mock_client:
424
+ # Create mock CoreV1Api
425
+ mock_core_v1_api = MagicMock()
426
+ mock_client.CoreV1Api.return_value = mock_core_v1_api
427
+
428
+ # Mock read_namespaced_pod_log to return logs
429
+ mock_core_v1_api.read_namespaced_pod_log.return_value = 'log line 1\nlog line 2\n'
430
+
431
+ # Get pod logs with all parameters
432
+ logs = k8s_apis.get_pod_logs(
433
+ pod_name='test-pod',
434
+ namespace='test-namespace',
435
+ container_name='test-container',
436
+ since_seconds=60,
437
+ tail_lines=100,
438
+ limit_bytes=1024,
439
+ )
440
+
441
+ # Verify the result
442
+ assert logs == 'log line 1\nlog line 2\n'
443
+
444
+ # Verify CoreV1Api was created with the correct API client
445
+ mock_client.CoreV1Api.assert_called_once_with(k8s_apis.api_client)
446
+
447
+ # Verify read_namespaced_pod_log was called with the correct parameters
448
+ mock_core_v1_api.read_namespaced_pod_log.assert_called_once_with(
449
+ name='test-pod',
450
+ namespace='test-namespace',
451
+ container='test-container',
452
+ since_seconds=60,
453
+ tail_lines=100,
454
+ limit_bytes=1024,
455
+ )
456
456
 
457
457
  def test_get_pod_logs_minimal(self, k8s_apis):
458
458
  """Test get_pod_logs method with minimal parameters."""
459
- # Mock the dynamic client and resources
460
- mock_resource = MagicMock()
461
- mock_log = MagicMock()
462
- mock_resource.log = mock_log
463
- mock_resources = MagicMock()
464
- mock_resources.get.return_value = mock_resource
465
- k8s_apis.dynamic_client.resources = mock_resources
466
-
467
- # Mock log.get to return logs
468
- mock_log.get.return_value = 'log line 1\nlog line 2\n'
469
-
470
- # Get pod logs with minimal parameters
471
- logs = k8s_apis.get_pod_logs(
472
- pod_name='test-pod',
473
- namespace='test-namespace',
474
- )
475
-
476
- # Verify the result
477
- assert logs == 'log line 1\nlog line 2\n'
478
-
479
- # Verify the dynamic client was used correctly
480
- mock_resources.get.assert_called_once_with(api_version='v1', kind='Pod')
481
- mock_log.get.assert_called_once_with(
482
- name='test-pod',
483
- namespace='test-namespace',
484
- )
459
+ # Mock the CoreV1Api client
460
+ with patch('kubernetes.client') as mock_client:
461
+ # Create mock CoreV1Api
462
+ mock_core_v1_api = MagicMock()
463
+ mock_client.CoreV1Api.return_value = mock_core_v1_api
464
+
465
+ # Mock read_namespaced_pod_log to return logs
466
+ mock_core_v1_api.read_namespaced_pod_log.return_value = 'log line 1\nlog line 2\n'
467
+
468
+ # Get pod logs with minimal parameters
469
+ logs = k8s_apis.get_pod_logs(
470
+ pod_name='test-pod',
471
+ namespace='test-namespace',
472
+ )
473
+
474
+ # Verify the result
475
+ assert logs == 'log line 1\nlog line 2\n'
476
+
477
+ # Verify CoreV1Api was created with the correct API client
478
+ mock_client.CoreV1Api.assert_called_once_with(k8s_apis.api_client)
479
+
480
+ # Verify read_namespaced_pod_log was called with the correct parameters
481
+ mock_core_v1_api.read_namespaced_pod_log.assert_called_once_with(
482
+ name='test-pod',
483
+ namespace='test-namespace',
484
+ )
485
485
 
486
486
  def _create_mock_event(self):
487
487
  """Create a mock event for testing."""
@@ -132,6 +132,72 @@ class TestK8sHandler:
132
132
  # Verify that the client was returned
133
133
  assert client == mock_client_cache.get_client.return_value
134
134
 
135
+ def test_load_yaml_template_removes_checkov_annotations(self, mock_mcp, mock_client_cache):
136
+ """Test _load_yaml_template method removes checkov skip annotations from deployment template."""
137
+ # Initialize the K8s handler
138
+ with patch(
139
+ 'awslabs.eks_mcp_server.k8s_handler.K8sClientCache', return_value=mock_client_cache
140
+ ):
141
+ handler = K8sHandler(mock_mcp)
142
+
143
+ # Create mock file content for templates with checkov skip annotations
144
+ deployment_template = """apiVersion: apps/v1
145
+ kind: Deployment
146
+ metadata:
147
+ name: APP_NAME
148
+ namespace: NAMESPACE
149
+ annotations:
150
+ checkov.io/skip1: "CKV_K8S_14=We're using a specific image version"
151
+ checkov.io/skip2: "CKV_K8S_43=Resource limits are set appropriately"
152
+ other-annotation: "This should be preserved"
153
+ spec:
154
+ replicas: REPLICAS"""
155
+
156
+ service_template = """apiVersion: v1
157
+ kind: Service
158
+ metadata:
159
+ name: APP_NAME
160
+ namespace: NAMESPACE
161
+ annotations:
162
+ service.beta.kubernetes.io/aws-load-balancer-scheme: LOAD_BALANCER_SCHEME"""
163
+
164
+ # Mock open to return our test templates
165
+ mock_open_func = mock_open()
166
+ mock_file = MagicMock()
167
+ mock_file.__enter__.return_value.read.side_effect = [deployment_template, service_template]
168
+ mock_open_func.return_value = mock_file
169
+
170
+ # Mock yaml.safe_load and yaml.dump to use real functions
171
+ with patch('builtins.open', mock_open_func):
172
+ # Test loading and processing templates
173
+ template_files = ['deployment.yaml', 'service.yaml']
174
+ values = {
175
+ 'APP_NAME': 'test-app',
176
+ 'NAMESPACE': 'test-namespace',
177
+ 'REPLICAS': '3',
178
+ 'LOAD_BALANCER_SCHEME': 'internal',
179
+ }
180
+
181
+ result = handler._load_yaml_template(template_files, values)
182
+
183
+ # Verify open was called for each template
184
+ assert mock_open_func.call_count == 2
185
+
186
+ # Verify template content was properly processed
187
+ assert 'kind: Deployment' in result
188
+ assert 'kind: Service' in result
189
+ assert 'name: test-app' in result
190
+ assert 'namespace: test-namespace' in result
191
+ assert 'replicas: 3' in result
192
+ assert 'service.beta.kubernetes.io/aws-load-balancer-scheme: internal' in result
193
+
194
+ # Verify checkov annotations were removed
195
+ assert 'checkov.io/skip1' not in result
196
+ assert 'checkov.io/skip2' not in result
197
+
198
+ # Verify other annotations were preserved
199
+ assert 'other-annotation: This should be preserved' in result
200
+
135
201
  @pytest.mark.asyncio
136
202
  async def test_apply_yaml_relative_path(self, mock_context, mock_mcp, mock_client_cache):
137
203
  """Test apply_yaml method with a relative path."""
@@ -886,74 +952,61 @@ metadata:
886
952
  ):
887
953
  handler = K8sHandler(mock_mcp, allow_write=True)
888
954
 
889
- # Prepare mock file content
890
- deployment_content = """apiVersion: apps/v1
891
- kind: Deployment
892
- metadata:
893
- name: APP_NAME
894
- namespace: NAMESPACE"""
955
+ # Mock the _load_yaml_template method to avoid template loading issues
956
+ with patch.object(handler, '_load_yaml_template', return_value='combined yaml content'):
957
+ # Mock os.path.isabs to return True for absolute paths
958
+ with patch('os.path.isabs', return_value=True):
959
+ # Mock os.makedirs to avoid creating directories
960
+ with patch('os.makedirs') as mock_makedirs:
961
+ # Mock open for writing output
962
+ with patch('builtins.open', mock_open()) as mocked_open:
963
+ # Mock os.path.abspath to return a predictable absolute path
964
+ with patch(
965
+ 'os.path.abspath',
966
+ return_value='/absolute/path/test-output/test-app-manifest.yaml',
967
+ ):
968
+ # Generate the manifest
969
+ result = await handler.generate_app_manifest(
970
+ mock_context,
971
+ app_name='test-app',
972
+ image_uri='123456789012.dkr.ecr.region.amazonaws.com/repo:tag',
973
+ port=8080,
974
+ replicas=3,
975
+ cpu='250m',
976
+ memory='256Mi',
977
+ namespace='test-namespace',
978
+ load_balancer_scheme='internet-facing',
979
+ output_dir='/absolute/path/test-output',
980
+ )
895
981
 
896
- service_content = """apiVersion: v1
897
- kind: Service
898
- metadata:
899
- name: APP_NAME
900
- namespace: NAMESPACE
901
- annotations:
902
- service.beta.kubernetes.io/aws-load-balancer-scheme: LOAD_BALANCER_SCHEME"""
982
+ # Verify that os.makedirs was called with exist_ok=True
983
+ mock_makedirs.assert_called_once_with(
984
+ '/absolute/path/test-output', exist_ok=True
985
+ )
903
986
 
904
- # Mock open function to return our test templates and for file writing
905
- mock_open = MagicMock()
906
- mock_file = MagicMock()
907
- mock_file.__enter__.return_value.read.side_effect = [deployment_content, service_content]
908
- mock_open.return_value = mock_file
987
+ # Verify that open was called for writing output
988
+ mocked_open.assert_called_once_with(
989
+ '/absolute/path/test-output/test-app-manifest.yaml', 'w'
990
+ )
909
991
 
910
- # Mock os.path.isabs to return True for absolute paths
911
- with patch('os.path.isabs', return_value=True):
912
- # Mock os.makedirs to avoid creating directories
913
- with patch('os.makedirs') as mock_makedirs:
914
- with patch('builtins.open', mock_open):
915
- # Mock os.path.abspath to return a predictable absolute path
916
- with patch(
917
- 'os.path.abspath',
918
- return_value='/absolute/path/test-output/test-app-manifest.yaml',
919
- ):
920
- # Generate the manifest
921
- result = await handler.generate_app_manifest(
922
- mock_context,
923
- app_name='test-app',
924
- image_uri='123456789012.dkr.ecr.region.amazonaws.com/repo:tag',
925
- port=8080,
926
- replicas=3,
927
- cpu='250m',
928
- memory='256Mi',
929
- namespace='test-namespace',
930
- load_balancer_scheme='internet-facing',
931
- output_dir='/absolute/path/test-output',
932
- )
933
-
934
- # Verify that os.makedirs was called with exist_ok=True
935
- mock_makedirs.assert_called_once_with(
936
- '/absolute/path/test-output', exist_ok=True
937
- )
938
-
939
- # Verify that open was called for reading templates and writing output
940
- assert mock_open.call_count == 3 # 2 reads + 1 write
941
-
942
- # Verify the result
943
- assert not result.isError
944
- assert isinstance(result.content[0], TextContent)
945
- assert 'Successfully generated YAML for test-app' in result.content[0].text
946
- assert (
947
- 'with image 123456789012.dkr.ecr.region.amazonaws.com/repo:tag'
948
- in result.content[0].text
949
- )
950
-
951
- # Verify that the output path is absolute
952
- assert os.path.isabs(result.output_file_path)
953
- assert (
954
- result.output_file_path
955
- == '/absolute/path/test-output/test-app-manifest.yaml'
956
- )
992
+ # Verify the result
993
+ assert not result.isError
994
+ assert isinstance(result.content[0], TextContent)
995
+ assert (
996
+ 'Successfully generated YAML for test-app'
997
+ in result.content[0].text
998
+ )
999
+ assert (
1000
+ 'with image 123456789012.dkr.ecr.region.amazonaws.com/repo:tag'
1001
+ in result.content[0].text
1002
+ )
1003
+
1004
+ # Verify that the output path is absolute
1005
+ assert os.path.isabs(result.output_file_path)
1006
+ assert (
1007
+ result.output_file_path
1008
+ == '/absolute/path/test-output/test-app-manifest.yaml'
1009
+ )
957
1010
 
958
1011
  @pytest.mark.asyncio
959
1012
  async def test_generate_app_manifest_error(self, mock_context, mock_mcp, mock_client_cache):
@@ -1705,6 +1758,67 @@ metadata:
1705
1758
  result = handler.cleanup_resource_response(simple_input)
1706
1759
  assert result == simple_input
1707
1760
 
1761
+ def test_remove_checkov_skip_annotations(self, mock_mcp, mock_client_cache):
1762
+ """Test _remove_checkov_skip_annotations method directly to ensure line 807 is covered."""
1763
+ # Initialize the K8s handler
1764
+ with patch(
1765
+ 'awslabs.eks_mcp_server.k8s_handler.K8sClientCache', return_value=mock_client_cache
1766
+ ):
1767
+ handler = K8sHandler(mock_mcp)
1768
+
1769
+ # Test case 1: YAML with only checkov skip annotations (should remove annotations completely)
1770
+ yaml_content = """apiVersion: apps/v1
1771
+ kind: Deployment
1772
+ metadata:
1773
+ name: test-app
1774
+ namespace: default
1775
+ annotations:
1776
+ checkov.io/skip1: "CKV_K8S_14=We're using a specific image version"
1777
+ checkov.io/skip2: "CKV_K8S_43=Resource limits are set appropriately"
1778
+ spec:
1779
+ replicas: 3"""
1780
+
1781
+ expected_result = """apiVersion: apps/v1
1782
+ kind: Deployment
1783
+ metadata:
1784
+ name: test-app
1785
+ namespace: default
1786
+ spec:
1787
+ replicas: 3
1788
+ """
1789
+
1790
+ result = handler._remove_checkov_skip_annotations(yaml_content)
1791
+ # Normalize whitespace for comparison
1792
+ result = result.replace(' ', '').replace('\n', '')
1793
+ expected_result = expected_result.replace(' ', '').replace('\n', '')
1794
+ assert result == expected_result
1795
+
1796
+ # Test case 2: YAML with mixed annotations (should keep non-checkov annotations)
1797
+ yaml_content = """apiVersion: apps/v1
1798
+ kind: Deployment
1799
+ metadata:
1800
+ name: test-app
1801
+ namespace: default
1802
+ annotations:
1803
+ checkov.io/skip1: "CKV_K8S_14=We're using a specific image version"
1804
+ other-annotation: "This should be preserved"
1805
+ spec:
1806
+ replicas: 3"""
1807
+
1808
+ expected_result = """apiVersion: apps/v1
1809
+ kind: Deployment
1810
+ metadata:
1811
+ name: test-app
1812
+ namespace: default
1813
+ annotations:
1814
+ other-annotation: This should be preserved
1815
+ spec:
1816
+ replicas: 3
1817
+ """
1818
+
1819
+ result = handler._remove_checkov_skip_annotations(yaml_content)
1820
+ assert 'other-annotation: This should be preserved' in result
1821
+
1708
1822
  def test_filter_null_values(self, mock_mcp, mock_client_cache):
1709
1823
  """Test filter_null_values method for removing null values from data structures."""
1710
1824
  # Initialize the K8s handler