Best Python code snippet using localstack_python
cloudformation_api.py
Source:cloudformation_api.py  
...161        return state162    def resource_status(self, resource_id: str):163        result = self._lookup(self.resource_states, resource_id)164        return result165    def latest_template_raw(self):166        if self.change_sets:167            return self.change_sets[-1]._template_raw168        return self._template_raw169    @property170    def resource_states(self):171        for resource_id in list(self._resource_states.keys()):172            self._set_resource_status_details(resource_id)173        return self._resource_states174    @property175    def stack_name(self):176        return self.metadata["StackName"]177    @property178    def stack_id(self):179        return self.metadata["StackId"]180    # TODO: potential performance issues due to many stack_parameters calls (cache or limit actual invocations)181    @property182    def resources(self):  # TODO: not actually resources, split apart183        """Return dict of resources, parameters, conditions, and other stack metadata."""184        result = dict(self.template_resources)185        def add_params(defaults=True):186            for param in self.stack_parameters(defaults=defaults):187                if param["ParameterKey"] not in result:188                    resolved_value = param.get("ResolvedValue")189                    result[param["ParameterKey"]] = {190                        "Type": "Parameter",191                        "LogicalResourceId": param["ParameterKey"],192                        "Properties": {193                            "Value": (194                                resolved_value195                                if resolved_value is not None196                                else param["ParameterValue"]197                            )198                        },199                    }200        add_params(defaults=False)201        # TODO: conditions and mappings don't really belong here and should be handled separately202        for name, value in self.conditions.items():203            if name not in result:204                result[name] = {205                    "Type": "Parameter",206                    "LogicalResourceId": name,207                    "Properties": {"Value": value},208                }209        for name, value in self.mappings.items():210            if name not in result:211                result[name] = {212                    "Type": "Parameter",213                    "LogicalResourceId": name,214                    "Properties": {"Value": value},215                }216        add_params(defaults=True)217        return result218    @property219    def template_resources(self):220        return self.template.setdefault("Resources", {})221    @property222    def tags(self):223        return aws_responses.extract_tags(self.metadata)224    @property225    def imports(self):226        def _collect(o, **kwargs):227            if isinstance(o, dict):228                import_val = o.get("Fn::ImportValue")229                if import_val:230                    result.add(import_val)231            return o232        result = set()233        recurse_object(self.resources, _collect)234        return result235    def outputs_list(self) -> List[Dict]:236        """Returns a copy of the outputs of this stack."""237        result = []238        for k, details in self.outputs.items():239            value = None240            try:241                template_deployer.resolve_refs_recursively(self, details)242                value = details["Value"]243            except Exception as e:244                LOG.debug("Unable to resolve references in stack outputs: %s - %s", details, e)245            exports = details.get("Export") or {}246            export = exports.get("Name")247            export = template_deployer.resolve_refs_recursively(self, export)248            description = details.get("Description")249            entry = {250                "OutputKey": k,251                "OutputValue": value,252                "Description": description,253                "ExportName": export,254            }255            result.append(entry)256        return result257    # TODO: check if metadata already populated/resolved and use it if possible (avoid unnecessary re-resolving)258    def stack_parameters(self, defaults=True) -> List[Dict[str, Any]]:259        result = {}260        # add default template parameter values261        if defaults:262            for key, value in self.template_parameters.items():263                param_value = value.get("Default")264                result[key] = {265                    "ParameterKey": key,266                    "ParameterValue": param_value,267                }268                # TODO: extract dynamic parameter resolving269                # TODO: support different types and refactor logic to use metadata (here not yet populated properly)270                param_type = value.get("Type", "")271                if not is_none_or_empty(param_type):272                    if param_type == "AWS::SSM::Parameter::Value<String>":273                        ssm_client = aws_stack.connect_to_service("ssm")274                        resolved_value = ssm_client.get_parameter(Name=param_value)["Parameter"][275                            "Value"276                        ]277                        result[key]["ResolvedValue"] = resolved_value278                    elif param_type.startswith("AWS::"):279                        LOG.info(280                            f"Parameter Type '{param_type}' is currently not supported. Coming soon, stay tuned!"281                        )282                    else:283                        # lets assume we support the normal CFn parameters284                        pass285        # add stack parameters286        result.update({p["ParameterKey"]: p for p in self.metadata["Parameters"]})287        # add parameters of change sets288        for change_set in self.change_sets:289            result.update({p["ParameterKey"]: p for p in change_set.metadata["Parameters"]})290        result = list(result.values())291        return result292    @property293    def template_parameters(self):294        return self.template["Parameters"]295    @property296    def conditions(self):297        """Returns the (mutable) dict of stack conditions."""298        return self.template.setdefault("Conditions", {})299    @property300    def mappings(self):301        """Returns the (mutable) dict of stack mappings."""302        return self.template.setdefault("Mappings", {})303    @property304    def outputs(self):305        """Returns the (mutable) dict of stack outputs."""306        return self.template.setdefault("Outputs", {})307    @property308    def exports_map(self):309        result = {}310        for export in CloudFormationRegion.get().exports:311            result[export["Name"]] = export312        return result313    @property314    def nested_stacks(self):315        """Return a list of nested stacks that have been deployed by this stack."""316        result = [317            r for r in self.template_resources.values() if r["Type"] == "AWS::CloudFormation::Stack"318        ]319        result = [find_stack(r["Properties"].get("StackName")) for r in result]320        result = [r for r in result if r]321        return result322    @property323    def status(self):324        return self.metadata["StackStatus"]325    @property326    def resource_types(self):327        return [r.get("Type") for r in self.template_resources.values()]328    def resource(self, resource_id):329        return self._lookup(self.resources, resource_id)330    def _lookup(self, resource_map, resource_id):331        resource = resource_map.get(resource_id)332        if not resource:333            raise Exception(334                'Unable to find details for resource "%s" in stack "%s"'335                % (resource_id, self.stack_name)336            )337        return resource338    def copy(self):339        return Stack(metadata=dict(self.metadata), template=dict(self.template))340class StackChangeSet(Stack):341    def __init__(self, params=None, template=None):342        if template is None:343            template = {}344        if params is None:345            params = {}346        super(StackChangeSet, self).__init__(params, template)347        name = self.metadata["ChangeSetName"]348        if not self.metadata.get("ChangeSetId"):349            self.metadata["ChangeSetId"] = aws_stack.cf_change_set_arn(350                name, change_set_id=short_uid()351            )352        stack = self.stack = find_stack(self.metadata["StackName"])353        self.metadata["StackId"] = stack.stack_id354        self.metadata["Status"] = "CREATE_PENDING"355    @property356    def change_set_id(self):357        return self.metadata["ChangeSetId"]358    @property359    def change_set_name(self):360        return self.metadata["ChangeSetName"]361    @property362    def resources(self):363        result = dict(self.stack.resources)364        result.update(self.resources)365        return result366    @property367    def changes(self):368        result = self.metadata["Changes"] = self.metadata.get("Changes", [])369        return result370class CloudFormationRegion(RegionBackend):371    def __init__(self):372        # maps stack ID to stack details373        self.stacks: Dict[str, Stack] = {}374        # maps stack set ID to stack set details375        self.stack_sets: Dict[str, StackSet] = {}376    @property377    def exports(self):378        exports = []379        output_keys = {}380        for stack_id, stack in self.stacks.items():381            for output in stack.outputs_list():382                export_name = output.get("ExportName")383                if not export_name:384                    continue385                if export_name in output_keys:386                    # TODO: raise exception on stack creation in case of duplicate exports387                    LOG.warning(388                        "Found duplicate export name %s in stacks: %s %s",389                        export_name,390                        output_keys[export_name],391                        stack.stack_id,392                    )393                entry = {394                    "ExportingStackId": stack.stack_id,395                    "Name": export_name,396                    "Value": output["OutputValue"],397                }398                exports.append(entry)399                output_keys[export_name] = stack.stack_id400        return exports401# --------------402# API ENDPOINTS403# --------------404def create_stack(req_params):405    state = CloudFormationRegion.get()406    template_deployer.prepare_template_body(req_params)  # TODO: avoid mutating req_params directly407    template = template_preparer.parse_template(req_params["TemplateBody"])408    stack_name = template["StackName"] = req_params.get("StackName")409    stack = Stack(req_params, template)410    # find existing stack with same name, and remove it if this stack is in DELETED state411    existing = ([s for s in state.stacks.values() if s.stack_name == stack_name] or [None])[0]412    if existing:413        if "DELETE" not in existing.status:414            return error_response(415                'Stack named "%s" already exists with status "%s"' % (stack_name, existing.status),416                code=400,417                code_string="ValidationError",418            )419        state.stacks.pop(existing.stack_id)420    state.stacks[stack.stack_id] = stack421    LOG.debug(422        'Creating stack "%s" with %s resources ...', stack.stack_name, len(stack.template_resources)423    )424    deployer = template_deployer.TemplateDeployer(stack)425    try:426        # TODO: create separate step to first resolve parameters427        deployer.deploy_stack()428    except Exception as e:429        stack.set_stack_status("CREATE_FAILED")430        msg = 'Unable to create stack "%s": %s' % (stack.stack_name, e)431        LOG.debug("%s %s", msg, traceback.format_exc())432        return error_response(msg, code=400, code_string="ValidationError")433    result = {"StackId": stack.stack_id}434    return result435def create_stack_set(req_params):436    state = CloudFormationRegion.get()437    stack_set = StackSet(req_params)438    stack_set_id = short_uid()439    stack_set.metadata["StackSetId"] = stack_set_id440    state.stack_sets[stack_set_id] = stack_set441    result = {"StackSetId": stack_set_id}442    return result443def create_stack_instances(req_params):444    state = CloudFormationRegion.get()445    set_name = req_params.get("StackSetName")446    stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]447    if not stack_set:448        return not_found_error('Stack set named "%s" does not exist' % set_name)449    stack_set = stack_set[0]450    op_id = req_params.get("OperationId") or short_uid()451    sset_meta = stack_set.metadata452    accounts = extract_url_encoded_param_list(req_params, "Accounts.member.%s")453    accounts = accounts or extract_url_encoded_param_list(454        req_params, "DeploymentTargets.Accounts.member.%s"455    )456    regions = extract_url_encoded_param_list(req_params, "Regions.member.%s")457    stacks_to_await = []458    for account in accounts:459        for region in regions:460            # deploy new stack461            LOG.debug('Deploying instance for stack set "%s" in region "%s"', set_name, region)462            cf_client = aws_stack.connect_to_service("cloudformation", region_name=region)463            kwargs = select_attributes(sset_meta, "TemplateBody") or select_attributes(464                sset_meta, "TemplateURL"465            )466            stack_name = "sset-%s-%s" % (set_name, account)467            result = cf_client.create_stack(StackName=stack_name, **kwargs)468            stacks_to_await.append((stack_name, region))469            # store stack instance470            instance = {471                "StackSetId": sset_meta["StackSetId"],472                "OperationId": op_id,473                "Account": account,474                "Region": region,475                "StackId": result["StackId"],476                "Status": "CURRENT",477                "StackInstanceStatus": {"DetailedStatus": "SUCCEEDED"},478            }479            instance = StackInstance(instance)480            stack_set.stack_instances.append(instance)481    # wait for completion of stack482    for stack in stacks_to_await:483        aws_stack.await_stack_completion(stack[0], region_name=stack[1])484    # record operation485    operation = {486        "OperationId": op_id,487        "StackSetId": stack_set.metadata["StackSetId"],488        "Action": "CREATE",489        "Status": "SUCCEEDED",490    }491    stack_set.operations[op_id] = operation492    result = {"OperationId": op_id}493    return result494def delete_stack(req_params):495    stack_name = req_params.get("StackName")496    stack = find_stack(stack_name)497    deployer = template_deployer.TemplateDeployer(stack)498    deployer.delete_stack()499    return {}500def delete_stack_set(req_params):501    state = CloudFormationRegion.get()502    set_name = req_params.get("StackSetName")503    stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]504    if not stack_set:505        return not_found_error('Stack set named "%s" does not exist' % set_name)506    for instance in stack_set[0].stack_instances:507        deployer = template_deployer.TemplateDeployer(instance.stack)508        deployer.delete_stack()509    return {}510def update_stack(req_params):511    stack_name = req_params.get("StackName")512    stack = find_stack(stack_name)513    if not stack:514        return not_found_error('Unable to update non-existing stack "%s"' % stack_name)515    template_preparer.prepare_template_body(req_params)516    template = template_preparer.parse_template(req_params["TemplateBody"])517    new_stack = Stack(req_params, template)518    deployer = template_deployer.TemplateDeployer(stack)519    try:520        deployer.update_stack(new_stack)521    except Exception as e:522        stack.set_stack_status("UPDATE_FAILED")523        msg = 'Unable to update stack "%s": %s' % (stack_name, e)524        LOG.debug("%s %s", msg, traceback.format_exc())525        return error_response(msg, code=400, code_string="ValidationError")526    result = {"StackId": stack.stack_id}527    return result528def update_stack_set(req_params):529    state = CloudFormationRegion.get()530    set_name = req_params.get("StackSetName")531    stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]532    if not stack_set:533        return not_found_error('Stack set named "%s" does not exist' % set_name)534    stack_set = stack_set[0]535    stack_set.metadata.update(req_params)536    op_id = req_params.get("OperationId") or short_uid()537    operation = {538        "OperationId": op_id,539        "StackSetId": stack_set.metadata["StackSetId"],540        "Action": "UPDATE",541        "Status": "SUCCEEDED",542    }543    stack_set.operations[op_id] = operation544    return {"OperationId": op_id}545def describe_stacks(req_params):546    state = CloudFormationRegion.get()547    stack_name = req_params.get("StackName")548    stack_list = list(state.stacks.values())549    stacks = [550        s.describe_details() for s in stack_list if stack_name in [None, s.stack_name, s.stack_id]551    ]552    if stack_name and not stacks:553        return error_response(554            "Stack with id %s does not exist" % stack_name,555            code=400,556            code_string="ValidationError",557        )558    result = {"Stacks": stacks}559    return result560def list_stacks(req_params):561    state = CloudFormationRegion.get()562    stack_status_filters = _get_status_filter_members(req_params)563    stacks = [564        s.describe_details()565        for s in state.stacks.values()566        if not stack_status_filters or s.status in stack_status_filters567    ]568    attrs = [569        "StackId",570        "StackName",571        "TemplateDescription",572        "CreationTime",573        "LastUpdatedTime",574        "DeletionTime",575        "StackStatus",576        "StackStatusReason",577        "ParentId",578        "RootId",579        "DriftInformation",580    ]581    stacks = [select_attributes(stack, attrs) for stack in stacks]582    result = {"StackSummaries": stacks}583    return result584def describe_stack_resource(req_params):585    stack_name = req_params.get("StackName")586    resource_id = req_params.get("LogicalResourceId")587    stack = find_stack(stack_name)588    if not stack:589        return stack_not_found_error(stack_name)590    details = stack.resource_status(resource_id)591    result = {"StackResourceDetail": details}592    return result593def describe_stack_resources(req_params):594    stack_name = req_params.get("StackName")595    resource_id = req_params.get("LogicalResourceId")596    phys_resource_id = req_params.get("PhysicalResourceId")597    if phys_resource_id and stack_name:598        return error_response("Cannot specify both StackName and PhysicalResourceId", code=400)599    # TODO: filter stack by PhysicalResourceId!600    stack = find_stack(stack_name)601    if not stack:602        return stack_not_found_error(stack_name)603    statuses = [604        res_status605        for res_id, res_status in stack.resource_states.items()606        if resource_id in [res_id, None]607    ]608    return {"StackResources": statuses}609def list_stack_resources(req_params):610    result = describe_stack_resources(req_params)611    if not isinstance(result, dict):612        return result613    result = {"StackResourceSummaries": result.pop("StackResources")}614    return result615def list_stack_instances(req_params):616    state = CloudFormationRegion.get()617    set_name = req_params.get("StackSetName")618    stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]619    if not stack_set:620        return not_found_error('Stack set named "%s" does not exist' % set_name)621    stack_set = stack_set[0]622    result = [inst.metadata for inst in stack_set.stack_instances]623    result = {"Summaries": result}624    return result625ChangeSetTypes = Literal["CREATE", "UPDATE", "IMPORT"]626def create_change_set(req_params: Dict[str, Any]):627    change_set_type: ChangeSetTypes = req_params.get("ChangeSetType", "UPDATE")628    stack_name: Optional[str] = req_params.get("StackName")629    change_set_name: Optional[str] = req_params.get("ChangeSetName")630    template_body: Optional[str] = req_params.get("TemplateBody")631    # s3 or secretsmanager url632    template_url: Optional[str] = req_params.get("TemplateUrl") or req_params.get("TemplateURL")633    if is_none_or_empty(change_set_name):634        return error_response(635            "ChangeSetName required", 400, "ValidationError"636        )  # TODO: check proper message637    if is_none_or_empty(stack_name):638        return error_response(639            "StackName required", 400, "ValidationError"640        )  # TODO: check proper message641    stack: Optional[Stack] = find_stack(stack_name)642    # validate and resolve template643    if template_body and template_url:644        return error_response(645            "Specify exactly one of 'TemplateBody' or 'TemplateUrl'", 400, "ValidationError"646        )  # TODO: check proper message647    if not template_body and not template_url:648        return error_response(649            "Specify exactly one of 'TemplateBody' or 'TemplateUrl'", 400, "ValidationError"650        )  # TODO: check proper message651    prepare_template_body(req_params)  # TODO: function has too many unclear responsibilities652    template = template_preparer.parse_template(req_params["TemplateBody"])653    del req_params["TemplateBody"]  # TODO: stop mutating req_params654    template["StackName"] = stack_name655    template[656        "ChangeSetName"657    ] = change_set_name  # TODO: validate with AWS what this is actually doing?658    if change_set_type == "UPDATE":659        # add changeset to existing stack660        if stack is None:661            return error_response(662                f"Stack '{stack_name}' does not exist.", 400, "ValidationError"663            )  # stack should exist already664    elif change_set_type == "CREATE":665        # create new (empty) stack666        if stack is not None:667            return error_response(668                f"Stack {stack_name} already exists", 400, "ValidationError"669            )  # stack should not exist yet (TODO: check proper message)670        state = CloudFormationRegion.get()671        empty_stack_template = dict(template)672        empty_stack_template["Resources"] = {}673        req_params_copy = clone_stack_params(req_params)674        stack = Stack(req_params_copy, empty_stack_template)675        state.stacks[stack.stack_id] = stack676        stack.set_stack_status("REVIEW_IN_PROGRESS")677    elif change_set_type == "IMPORT":678        raise NotImplementedError()  # TODO: implement importing resources679    else:680        msg = f"1 validation error detected: Value '{change_set_type}' at 'changeSetType' failed to satisfy constraint: Member must satisfy enum value set: [IMPORT, UPDATE, CREATE]"681        return error_response(msg, code=400, code_string="ValidationError")682    change_set = StackChangeSet(req_params, template)683    # TODO: refactor the flow here684    deployer = template_deployer.TemplateDeployer(change_set)685    deployer.construct_changes(686        stack,687        change_set,688        change_set_id=change_set.change_set_id,689        append_to_changeset=True,690    )  # TODO: ignores return value (?)691    deployer.apply_parameter_changes(change_set, change_set)  # TODO: bandaid to populate metadata692    stack.change_sets.append(change_set)693    change_set.metadata[694        "Status"695    ] = "CREATE_COMPLETE"  # technically for some time this should first be CREATE_PENDING696    change_set.metadata[697        "ExecutionStatus"698    ] = "AVAILABLE"  # technically for some time this should first be UNAVAILABLE699    return {"StackId": change_set.stack_id, "Id": change_set.change_set_id}700def execute_change_set(req_params):701    stack_name = req_params.get("StackName")702    cs_name = req_params.get("ChangeSetName")703    change_set = find_change_set(cs_name, stack_name=stack_name)704    if not change_set:705        return not_found_error(706            'Unable to find change set "%s" for stack "%s"' % (cs_name, stack_name)707        )708    LOG.debug(709        'Executing change set "%s" for stack "%s" with %s resources ...',710        cs_name,711        stack_name,712        len(change_set.template_resources),713    )714    deployer = template_deployer.TemplateDeployer(change_set.stack)715    deployer.apply_change_set(change_set)716    change_set.stack.metadata["ChangeSetId"] = change_set.change_set_id717    return {}718def list_change_sets(req_params):719    stack_name = req_params.get("StackName")720    stack = find_stack(stack_name)721    if not stack:722        return not_found_error('Unable to find stack "%s"' % stack_name)723    result = [cs.metadata for cs in stack.change_sets]724    result = {"Summaries": result}725    return result726def list_stack_sets(req_params):727    state = CloudFormationRegion.get()728    result = [sset.metadata for sset in state.stack_sets.values()]729    result = {"Summaries": result}730    return result731def describe_change_set(req_params):732    stack_name = req_params.get("StackName")733    cs_name = req_params.get("ChangeSetName")734    change_set: Optional[StackChangeSet] = find_change_set(cs_name, stack_name=stack_name)735    if not change_set:736        return not_found_error(737            'Unable to find change set "%s" for stack "%s"' % (cs_name, stack_name)738        )739    return change_set.metadata740def describe_stack_set(req_params):741    state = CloudFormationRegion.get()742    set_name = req_params.get("StackSetName")743    result = [744        sset.metadata for sset in state.stack_sets.values() if sset.stack_set_name == set_name745    ]746    if not result:747        return not_found_error('Unable to find stack set "%s"' % set_name)748    result = {"StackSet": result[0]}749    return result750def describe_stack_set_operation(req_params):751    state = CloudFormationRegion.get()752    set_name = req_params.get("StackSetName")753    stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]754    if not stack_set:755        return not_found_error('Unable to find stack set "%s"' % set_name)756    stack_set = stack_set[0]757    op_id = req_params.get("OperationId")758    result = stack_set.operations.get(op_id)759    if not result:760        LOG.debug(761            'Unable to find operation ID "%s" for stack set "%s" in list: %s',762            op_id,763            set_name,764            list(stack_set.operations.keys()),765        )766        return not_found_error(767            'Unable to find operation ID "%s" for stack set "%s"' % (op_id, set_name)768        )769    result = {"StackSetOperation": result}770    return result771def list_exports(req_params):772    state = CloudFormationRegion.get()773    result = {"Exports": state.exports}774    return result775def list_imports(req_params):776    state = CloudFormationRegion.get()777    export_name = req_params.get("ExportName")778    importing_stack_names = []779    for stack in state.stacks.values():780        if export_name in stack.imports:781            importing_stack_names.append(stack.stack_name)782    result = {"Imports": importing_stack_names}783    return result784def validate_template(req_params):785    try:786        result = template_preparer.validate_template(req_params)787        result = "<tmp>%s</tmp>" % result788        result = xmltodict.parse(result)["tmp"]789        return result790    except Exception as err:791        return error_response("Template Validation Error: %s" % err)792def describe_stack_events(req_params):793    stack_name = req_params.get("StackName")794    state = CloudFormationRegion.get()795    events = []796    for stack_id, stack in state.stacks.items():797        if stack_name in [None, stack.stack_name, stack.stack_id]:798            events.extend(stack.events)799    return {"StackEvents": events}800def delete_change_set(req_params):801    stack_name = req_params.get("StackName")802    cs_name = req_params.get("ChangeSetName")803    change_set = find_change_set(cs_name, stack_name=stack_name)804    if not change_set:805        return not_found_error(806            'Unable to find change set "%s" for stack "%s"' % (cs_name, stack_name)807        )808    change_set.stack.change_sets = [809        cs for cs in change_set.stack.change_sets if cs.change_set_name != cs_name810    ]811    return {}812def get_template(req_params):813    stack_name = req_params.get("StackName")814    cs_name = req_params.get("ChangeSetName")815    stack = find_stack(stack_name)816    if cs_name:817        stack = find_change_set(stack_name=stack_name, cs_name=cs_name)818    if not stack:819        return stack_not_found_error(stack_name)820    result = {"TemplateBody": json.dumps(stack.latest_template_raw())}821    return result822def get_template_summary(req_params):823    stack_name = req_params.get("StackName")824    stack = None825    if stack_name:826        stack = find_stack(stack_name)827        if not stack:828            return stack_not_found_error(stack_name)829    else:830        template_deployer.prepare_template_body(req_params)831        template = template_preparer.parse_template(req_params["TemplateBody"])832        req_params["StackName"] = "tmp-stack"833        stack = Stack(req_params, template)834    result = stack.describe_details()...Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!
