Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add on_exists param with behaviour fail, ignore and overwrite #207

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,14 +301,34 @@ params:

**Note**: If duplicate parameters are provided, the parameters provided as key-value pairs inside the `params` nested dictionary of the YAML file will take precedence **over** values in the provided `params-file`.

### 2. `overwrite` Functionality
### 2. `on_exists` Functionality

For every entity defined in your YAML file, you can specify `overwrite: True` to overwrite any existing entities in Seqera Platform of the same name.
For every entity defined in your YAML file, you can specify how to handle cases where the entity already exists using the `on_exists` parameter with one of three options:

`seqerakit` will first check to see if the name of the entity exists, if so, it will invoke a `tw <subcommand> delete` command before attempting to create it based on the options defined in the YAML file.
- `fail` (default): Raise an error if the entity already exists
- `ignore`: Skip creation if the entity already exists
- `overwrite`: Delete the existing entity and create a new one based on the YAML configuration

Example usage in YAML:

```yaml
workspaces:
- name: 'showcase'
organization: 'seqerakit_automation'
on_exists: 'overwrite' # Will delete and recreate if exists
```

```yaml
credentials:
- name: 'github_credentials'
workspace: 'seqerakit_automation/showcase'
on_exists: 'ignore' # Will skip if already exists
```

When using the `overwrite` option, `seqerakit` will first check if the entity exists, and if so, it will invoke a `tw <subcommand> delete` command before attempting to create it based on the options defined in the YAML file:

```console
DEBUG:root: Overwrite is set to 'True' for organizations
DEBUG:root: on_exists is set to 'overwrite' for organizations

DEBUG:root: Running command: tw -o json organizations list
DEBUG:root: The attempted organizations resource already exists. Overwriting.
Expand All @@ -317,6 +337,8 @@ DEBUG:root: Running command: tw organizations delete --name $SEQERA_ORGANIZATION
DEBUG:root: Running command: tw organizations add --name $SEQERA_ORGANIZATION_NAME --full-name $SEQERA_ORGANIZATION_NAME --description 'Example of an organization'
```

> **Note**: For backward compatibility, the `overwrite: True|False` parameter is still supported but deprecated. It will be mapped to `on_exists: 'overwrite'|'fail'` respectively.

### 3. Specifying JSON configuration files with `file-path`

The Seqera Platform CLI allows export and import of entities through JSON configuration files for pipelines and compute environments. To use these files to add a pipeline or compute environment to a workspace, use the `file-path` key to specify a path to a JSON configuration file.
Expand Down
75 changes: 63 additions & 12 deletions seqerakit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import logging
import sys
import os
import yaml
import yaml # type: ignore

from pathlib import Path

Expand All @@ -32,13 +32,14 @@
CommandError,
)
from seqerakit import __version__
from seqerakit.on_exists import OnExists

logger = logging.getLogger(__name__)


def parse_args(args=None):
parser = argparse.ArgumentParser(
description="Seqerakit: Python wrapper for the Seqera Platform CLI"
description="Build a Seqera Platform instance from a YAML configuration file."
)
# General options
general = parser.add_argument_group("General Options")
Expand Down Expand Up @@ -106,10 +107,20 @@ def parse_args(args=None):
type=str,
help="Path to a YAML file containing environment variables for configuration.",
)
yaml_processing.add_argument(
"--on-exists",
dest="on_exists",
type=str,
help="Globally specifies the action to take if a resource already exists.",
choices=[e.name.lower() for e in OnExists],
)
yaml_processing.add_argument(
"--overwrite",
action="store_true",
help="Globally enable overwrite for all resources defined in YAML input(s).",
help="""
Globally enable overwrite for all resources defined in YAML input(s).
"Deprecated: Please use '--on-exists=overwrite' instead.""",
deprecated=True,
)
return parser.parse_args(args)

Expand Down Expand Up @@ -147,7 +158,7 @@ def handle_block(self, block, args, destroy=False, dryrun=False):
if destroy:
logging.debug(" The '--delete' flag has been specified.\n")
self.overwrite_method.handle_overwrite(
block, args["cmd_args"], overwrite=False, destroy=True
block, args["cmd_args"], on_exists=OnExists.FAIL, destroy=True
)
return

Expand All @@ -162,16 +173,46 @@ def handle_block(self, block, args, destroy=False, dryrun=False):
),
}

# Check if overwrite is set to True or globally, and call overwrite handler
overwrite_option = args.get("overwrite", False) or getattr(
self.sp, "overwrite", False
)
if overwrite_option and not dryrun:
logging.debug(f" Overwrite is set to 'True' for {block}\n")
self.overwrite_method.handle_overwrite(
block, args["cmd_args"], overwrite_option
# Determine the on_exists behavior (default to FAIL)
on_exists = OnExists.FAIL

# Check for global settings (they override block-level settings)
if (
hasattr(self.sp, "global_on_exists")
and self.sp.global_on_exists is not None
):
on_exists = self.sp.global_on_exists
elif getattr(self.sp, "overwrite", False):
logging.warning(
"The '--overwrite' flag is deprecated. "
"Please use '--on-exists=overwrite' instead."
)
on_exists = OnExists.OVERWRITE

# If no global setting, use block-level setting if provided
elif "on_exists" in args:
on_exists_value = args["on_exists"]
if isinstance(on_exists_value, str):
try:
on_exists = OnExists[on_exists_value.upper()]
except KeyError as err:
logging.error(f"Invalid on_exists option: {on_exists_value}")
raise err

if not dryrun:
# Use on_exists.name.lower() only if it's an enum, otherwise use the string
on_exists_str = (
on_exists.name.lower() if hasattr(on_exists, "name") else on_exists
)
logging.debug(f" on_exists is set to '{on_exists_str}' for {block}\n")
should_continue = self.overwrite_method.handle_overwrite(
block, args["cmd_args"], on_exists=on_exists
)

# If on_exists is "ignore" and resource exists, skip creation
if not should_continue:
return

if block in self.list_for_add_method:
helper.handle_generic_block(self.sp, block, args["cmd_args"])
elif block in block_handler_map:
Expand Down Expand Up @@ -248,6 +289,16 @@ def main(args=None):
)
sp.overwrite = options.overwrite # If global overwrite is set

# Set global on_exists parameter if provided
if options.on_exists:
try:
sp.global_on_exists = OnExists[options.on_exists.upper()]
except KeyError:
logging.error(f"Invalid on_exists option: {options.on_exists}")
raise
else:
sp.global_on_exists = None

# If the info flag is set, run 'tw info'
try:
if options.info:
Expand Down
29 changes: 25 additions & 4 deletions seqerakit/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
Including handling methods for each block in the YAML file, and parsing
methods for each block in the YAML file.
"""
import yaml
import yaml # type: ignore
from seqerakit import utils
import sys
import json
from seqerakit.on_exists import OnExists


def parse_yaml_block(yaml_data, block_name):
Expand Down Expand Up @@ -163,11 +164,31 @@ def parse_block(block_name, item):
}
# Use the generic block function as a default.
parse_fn = block_to_function.get(block_name, parse_generic_block)
overwrite = item.pop("overwrite", False)

# Call the appropriate function and return its result along with overwrite value.
# Get on_exists setting with backward compatibility for overwrite
overwrite = item.pop("overwrite", None)
on_exists_str = item.pop("on_exists", "fail")

# Determine final on_exists value
if overwrite is not None:
# overwrite takes precedence for backward compatibility
on_exists = OnExists.OVERWRITE if overwrite else OnExists.FAIL
elif isinstance(on_exists_str, str):
try:
on_exists = OnExists[on_exists_str.upper()]
except KeyError:
raise ValueError(
f"Invalid on_exists option: '{on_exists_str}'. "
f"Valid options are: "
f"{', '.join(behaviour.name.lower() for behaviour in OnExists)}"
)
else:
# Use directly if already an enum
on_exists = on_exists_str

# Parse the block and return with on_exists value
cmd_args = parse_fn(item)
return {"cmd_args": cmd_args, "overwrite": overwrite}
return {"cmd_args": cmd_args, "on_exists": on_exists.name.lower()}


# Parsers for certain blocks of yaml that require handling
Expand Down
9 changes: 9 additions & 0 deletions seqerakit/on_exists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import Enum, auto


class OnExists(Enum):
"""Enum defining behavior when a resource already exists."""

FAIL = auto()
IGNORE = auto()
OVERWRITE = auto()
56 changes: 47 additions & 9 deletions seqerakit/overwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import json
from seqerakit import utils
from seqerakit.seqeraplatform import ResourceExistsError
from seqerakit.on_exists import OnExists
import logging


Expand All @@ -37,6 +38,9 @@ class Overwrite:
"studios",
]

# Define valid on_exists options as enum values
VALID_ON_EXISTS_OPTIONS = [e.name.lower() for e in OnExists]

def __init__(self, sp):
"""
Initializes an Overwrite instance.
Expand Down Expand Up @@ -95,11 +99,36 @@ def __init__(self, sp):
},
}

def handle_overwrite(self, block, args, overwrite=False, destroy=False):
def handle_overwrite(
self, block, args, on_exists=OnExists.FAIL, destroy=False, overwrite=None
):
"""
Handles overwrite functionality for Seqera Platform resources and
calling the 'tw delete' method with the correct args.
Handles resource existence behavior for Seqera Platform resources.

Args:
block: The resource block type (e.g., "organizations", "teams")
args: Command line arguments for the resource
on_exists: How to handle existing resources. Options:
- OnExists.FAIL (default): Raise an error if resource exists
- OnExists.IGNORE: Skip creation if resource exists
- OnExists.OVERWRITE: Delete existing resource and create new one
destroy: Whether to delete the resource
overwrite: Legacy parameter for backward compatibility
"""
# Convert string to enum if needed
if isinstance(on_exists, str):
try:
on_exists = OnExists[on_exists.upper()]
except KeyError:
raise ValueError(
f"Invalid on_exists option: {on_exists}. "
f"Valid options are: {', '.join(self.VALID_ON_EXISTS_OPTIONS)}"
)

# For backward compatibility
if overwrite is not None:
on_exists = OnExists.OVERWRITE if overwrite else OnExists.FAIL

if block in Overwrite.generic_deletion:
self.block_operations[block] = {
"keys": ["name", "workspace"],
Expand All @@ -122,23 +151,32 @@ def handle_overwrite(self, block, args, overwrite=False, destroy=False):
elif block == "members":
# Rename the user key to name to correctly index JSON data
sp_args["name"] = sp_args.pop("user")

if self.check_resource_exists(operation["name_key"], sp_args):
# if resource exists and overwrite is true, delete
if overwrite:
# Handle based on on_exists parameter
if on_exists == OnExists.OVERWRITE:
logging.info(
f" The attempted {block} resource already exists."
" Overwriting.\n"
)
self.delete_resource(block, operation, sp_args)
elif on_exists == OnExists.IGNORE:
logging.info(
f" The {block} resource already exists." " Skipping creation.\n"
)
return False
elif destroy:
logging.info(f" Deleting the {block} resource.")
self.delete_resource(block, operation, sp_args)
else: # return an error if resource exists, overwrite=False
else: # fail
raise ResourceExistsError(
f" The {block} resource already exists and"
" will not be created. Please set 'overwrite: True'"
" in your config file.\n"
f"The {block} resource already exists and "
"will not be created. Please set 'on_exists: overwrite' "
"to replace the resource or set 'on_exists: ignore' to "
"ignore this error.\n"
)
return True
return True

def _get_organization_args(self, args):
"""
Expand Down
2 changes: 1 addition & 1 deletion templates/teams.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ teams:
description: 'My test team' # optional
members: # optional
- '[email protected]'
overwrite: True # optional
on_exists: 'overwrite' # optional - values: 'fail' (default), 'ignore', 'overwrite'
Loading