From cf486a45a2988b5f282699b2a1ec5d4295af16ab Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 1 Jan 2021 00:34:05 +0900 Subject: [PATCH 01/21] Initial code commit (#1) --- .editorconfig | 429 ++++++++++++++++++ .gitignore | 6 + .vscode/extensions.json | 6 + .vscode/launch.json | 33 ++ .vscode/settings.json | 7 + .vscode/tasks.json | 82 ++++ WebSubSubscriptionHandler.sln | 56 +++ resources/azuredeploy.bicep | 235 ++++++++++ resources/azuredeploy.json | 264 +++++++++++ resources/azuredeploy.parameters.json | 9 + .../.gitignore | 264 +++++++++++ .../.vscode/extensions.json | 6 + .../.vscode/launch.json | 11 + .../.vscode/settings.json | 7 + .../.vscode/tasks.json | 69 +++ .../CallbackHttpTrigger.cs | 92 ++++ .../Constants/CallbackKeys.cs | 33 ++ .../Constants/HttpTriggerKeys.cs | 18 + .../Constants/SubscriptionKeys.cs | 48 ++ .../Extensions/EnumExtensions.cs | 28 ++ .../Models/AppSettings.cs | 63 +++ .../Models/CallbackResponse.cs | 17 + .../Models/Response.cs | 15 + .../Models/SubscriptionMode.cs | 32 ++ .../Models/SubscriptionRequest.cs | 22 + .../Models/SubscriptionResponse.cs | 17 + .../Models/SubscriptionVerificationQuery.cs | 85 ++++ .../Models/SubscriptionVerificationType.cs | 32 ++ .../Models/VerificationResponse.cs | 18 + .../Models/YouTubeVideoEventType.cs | 32 ++ .../Services/CallbackService.cs | 106 +++++ .../Services/ICallbackService.cs | 32 ++ .../Services/ISubscriptionService.cs | 19 + .../Services/SubscriptionService.cs | 59 +++ .../StartUp.cs | 51 +++ .../SubscriptionHttpTrigger.cs | 70 +++ ...bSubSubscriptionHandler.FunctionApp.csproj | 28 ++ .../host.json | 11 + .../local.settings.samples.json | 13 + .../CallbackHttpTriggerTests.cs | 20 + .../SubscriptionHttpTriggerTests.cs | 20 + ...bscriptionHandler.FunctionApp.Tests.csproj | 28 ++ 42 files changed, 2493 insertions(+) create mode 100644 .editorconfig create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 WebSubSubscriptionHandler.sln create mode 100644 resources/azuredeploy.bicep create mode 100644 resources/azuredeploy.json create mode 100644 resources/azuredeploy.parameters.json create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/.gitignore create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/extensions.json create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/launch.json create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/settings.json create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/tasks.json create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/CallbackHttpTrigger.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/CallbackKeys.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/HttpTriggerKeys.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/SubscriptionKeys.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Extensions/EnumExtensions.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/CallbackResponse.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/Response.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionMode.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionRequest.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionResponse.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationQuery.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationType.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VerificationResponse.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/YouTubeVideoEventType.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ICallbackService.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ISubscriptionService.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/SubscriptionHttpTrigger.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/host.json create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json create mode 100644 test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/CallbackHttpTriggerTests.cs create mode 100644 test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/SubscriptionHttpTriggerTests.cs create mode 100644 test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests.csproj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2935645 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,429 @@ +# Version: 1.3.1 (Using https://semver.org/) +# Updated: 2019-08-04 +# See https://github.com/RehanSaeed/EditorConfig/releases for release notes. +# See https://github.com/RehanSaeed/EditorConfig for updates to this file. +# See http://EditorConfig.org for more information about .editorconfig files. + +########################################## +# Common Settings +########################################## + +# This file is the top-most EditorConfig file +root = true + +# All Files +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +########################################## +# File Extension Settings +########################################## + +# Visual Studio Solution Files +[*.sln] +indent_style = tab + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Various XML Configuration Files +[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# JSON Files +[*.{json,json5}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.md] +trim_trailing_whitespace = false + +# Web Files +[*.{htm,html,js,ts,tsx,css,sass,scss,less,svg,vue}] +indent_size = 2 + +# Batch Files +[*.{cmd,bat}] + +# Bash Files +[*.sh] +end_of_line = lf + +# Bicep files +[*.bicep] +indent_size = 4 + +########################################## +# .NET Language Conventions +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions +########################################## + +# .NET Code Style Settings +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings +[*.{cs,csx,cake,vb}] +# "this." and "Me." qualifiers +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me +dotnet_style_qualification_for_field = true:warning +dotnet_style_qualification_for_property = true:warning +dotnet_style_qualification_for_method = true:warning +dotnet_style_qualification_for_event = true:warning +# Language keywords instead of framework type names for type references +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +# Modifier preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers +dotnet_style_require_accessibility_modifiers = always:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async +dotnet_style_readonly_field = true:warning +# Parentheses preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +# Expression-level preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion +dotnet_style_prefer_conditional_expression_over_return = false:suggestion +dotnet_style_prefer_compound_assignment = true:warning +# Null-checking preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +# Parameter preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences +dotnet_code_quality_unused_parameters = all:warning +# More style options (Undocumented) +# https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641 +dotnet_style_operator_placement_when_wrapping = end_of_line + +# C# Code Style Settings +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings +[*.{cs,csx,cake}] +# Implicit and explicit types +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning +# Expression-bodied members +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members +# IDE0022 +csharp_style_expression_bodied_methods = false:silent +# IDE0021 +csharp_style_expression_bodied_constructors = false:silent +# IDE0023, IDE0024 +csharp_style_expression_bodied_operators = false:silent +# IDE0025 +csharp_style_expression_bodied_properties = true:silent +# IDE0026 +csharp_style_expression_bodied_indexers = true:silent +# IDE0027 +csharp_style_expression_bodied_accessors = true:silent +# IDE0053 +csharp_style_expression_bodied_lambdas = true:silent +# IDE0061 +csharp_style_expression_bodied_local_functions = false:silent +# Pattern matching +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +# Inlined variable declarations +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations +csharp_style_inlined_variable_declaration = true:warning +# Expression-level preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences +csharp_prefer_simple_default_expression = true:warning +# "Null" checking preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences +csharp_style_throw_expression = true:warning +csharp_style_conditional_delegate_call = true:warning +# Code block preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences +csharp_prefer_braces = true:warning +# Unused value preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences +csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +# Index and range preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +# Miscellaneous preferences +# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +csharp_using_directive_placement = outside_namespace:warning +csharp_prefer_static_local_function = true:warning +csharp_prefer_simple_using_statement = false:warning + +########################################## +# .NET Formatting Conventions +# https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions +########################################## + +# Organize usings +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives +dotnet_sort_system_directives_first = always:error +dotnet_separate_import_directive_groups = always:error +# Newline options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false +# Spacing options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false +# Wrapping options +# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +########################################## +# .NET Naming Conventions +# https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions +########################################## + +[*.{cs,csx,cake,vb}] + +########################################## +# Styles +########################################## + +# camel_case_style - Define the camelCase style +dotnet_naming_style.camel_case_style.capitalization = camel_case +# pascal_case_style - Define the PascalCase style +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# first_upper_style - The first character must start with an upper-case character +dotnet_naming_style.first_upper_style.capitalization = first_word_upper +# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' +dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case +dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I +# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' +dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case +dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T +# disallowed_style - Anything that has this style applied is marked as disallowed +dotnet_naming_style.disallowed_style.capitalization = pascal_case +dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ +dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ +# internal_error_style - This style should never occur... if it does, it's indicates a bug in file or in the parser using the file +dotnet_naming_style.internal_error_style.capitalization = pascal_case +dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ +dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ + +########################################## +# .NET Design Guideline Field Naming Rules +# Naming rules for fields follow the .NET Framework design guidelines +# https://docs.microsoft.com/dotnet/standard/design-guidelines/index +########################################## + +# All public/protected/protected_internal constant fields must be PascalCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning + +# All public/protected/protected_internal static readonly fields must be PascalCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning + +# No other public/protected/protected_internal fields are allowed +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error + +########################################## +# StyleCop Field Naming Rules +# Naming rules for fields follow the StyleCop analyzers +# This does not override any rules using disallowed_style above +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers +########################################## + +# All constant fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning + +# All static readonly fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning + +# No non-private instance fields are allowed +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error + +# Private fields must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning + +# Local variables must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent + +# This rule should never fire. However, it's included for at least two purposes: +# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. +# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error + + +########################################## +# Other Naming Rules +########################################## + +# All of the following must be PascalCase: +# - Namespaces +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Classes and Enumerations +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Delegates +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types +# - Constructors, Properties, Events, Methods +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members +dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property +dotnet_naming_rule.element_rule.symbols = element_group +dotnet_naming_rule.element_rule.style = pascal_case_style +dotnet_naming_rule.element_rule.severity = warning + +# Interfaces use PascalCase and are prefixed with uppercase 'I' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.interface_group.applicable_kinds = interface +dotnet_naming_rule.interface_rule.symbols = interface_group +dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style +dotnet_naming_rule.interface_rule.severity = warning + +# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter +dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group +dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style +dotnet_naming_rule.type_parameter_rule.severity = warning + +# Function parameters use camelCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters +dotnet_naming_symbols.parameters_group.applicable_kinds = parameter +dotnet_naming_rule.parameters_rule.symbols = parameters_group +dotnet_naming_rule.parameters_rule.style = camel_case_style +dotnet_naming_rule.parameters_rule.severity = warning + +########################################## +# License +########################################## +# The following applies as to the .editorconfig file ONLY, and is +# included below for reference, per the requirements of the license +# corresponding to this .editorconfig file. +# See: https://github.com/RehanSaeed/EditorConfig +# +# MIT License +# +# Copyright (c) 2017-2019 Muhammad Rehan Saeed +# Copyright (c) 2019 Henry Gabryjelski +# +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject +# to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +########################################## diff --git a/.gitignore b/.gitignore index dfcfd56..855d03a 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,9 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Project-specific +azurite/ + +.DS_Store +*.local.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..de991f4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..02d2534 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/bin/Debug/netcoreapp3.1/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests.dll", + "args": [], + "cwd": "${workspaceFolder}/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + }, + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b1761ee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "azureFunctions.deploySubpath": "src/YouTubeWebSubSubscriptionHandler.FunctionApp/bin/Release/netcoreapp3.1/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~3", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "publish" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..fb6f112 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,82 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "build", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "clean release", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release", + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + // "${workspaceFolder}/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "type": "func", + "dependsOn": "build", + "options": { + "cwd": "${workspaceFolder}/src/YouTubeWebSubSubscriptionHandler.FunctionApp/bin/Debug/netcoreapp3.1" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" + } + ] +} diff --git a/WebSubSubscriptionHandler.sln b/WebSubSubscriptionHandler.sln new file mode 100644 index 0000000..13d119e --- /dev/null +++ b/WebSubSubscriptionHandler.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{07A1C98C-46F1-4610-A09C-2A7D0B37A56E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YouTubeWebSubSubscriptionHandler.FunctionApp", "src\YouTubeWebSubSubscriptionHandler.FunctionApp\YouTubeWebSubSubscriptionHandler.FunctionApp.csproj", "{97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1414324E-69D9-443F-9BFE-BAA767450B63}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YouTubeWebSubSubscriptionHandler.FunctionApp.Tests", "test\YouTubeWebSubSubscriptionHandler.FunctionApp.Tests\YouTubeWebSubSubscriptionHandler.FunctionApp.Tests.csproj", "{D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Debug|x64.Build.0 = Debug|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Debug|x86.Build.0 = Debug|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Release|Any CPU.Build.0 = Release|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Release|x64.ActiveCfg = Release|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Release|x64.Build.0 = Release|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Release|x86.ActiveCfg = Release|Any CPU + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5}.Release|x86.Build.0 = Release|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Debug|x64.Build.0 = Debug|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Debug|x86.Build.0 = Debug|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Release|Any CPU.Build.0 = Release|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Release|x64.ActiveCfg = Release|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Release|x64.Build.0 = Release|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Release|x86.ActiveCfg = Release|Any CPU + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {97A60B1C-BDB2-4B3F-9EF5-7B792F2D72C5} = {07A1C98C-46F1-4610-A09C-2A7D0B37A56E} + {D43CD661-1AC7-4FFE-9DE8-1E82DB2784C2} = {1414324E-69D9-443F-9BFE-BAA767450B63} + EndGlobalSection +EndGlobal diff --git a/resources/azuredeploy.bicep b/resources/azuredeploy.bicep new file mode 100644 index 0000000..ed3533a --- /dev/null +++ b/resources/azuredeploy.bicep @@ -0,0 +1,235 @@ +// Resource name +param name string + +// Provisioning environment +param env string { + allowed: [ + 'dev' + 'test' + 'prod' + ] + default: 'dev' +} + +// Resource location +param location string = resourceGroup().location + +// Resource location code +param locationCode string = 'krc' + +// Storage Account +param storageAccountSku string = 'Standard_LRS' + +// Event Grid +param eventGridInputSchema string { + allowed: [ + 'CloudEventSchemaV1_0' + 'CustomEventSchema' + 'EventGridSchema' + ] + default: 'CloudEventSchemaV1_0' +} +param eventGridOutputSchema string { + allowed: [ + 'CloudEventSchemaV1_0' + 'CustomInputSchema' + 'EventGridSchema' + ] + default: 'CloudEventSchemaV1_0' +} + +// Function App +param functionAppWorkerRuntime string = 'dotnet' +param functionAppEnvironment string { + allowed: [ + 'Development' + 'Staging' + 'Production' + ] + default: 'Development' +} +param functionAppTimezone string = 'Korea Standard Time' + +// WebSub +param websubSubscriptionUri string = 'https://pubsubhubbub.appspot.com/subscribe' +param websubCallbackEndpoint string = 'api/callback' + +var metadata = { + longName: '{0}-${name}{1}-${env}-${locationCode}' + shortName: '{0}${name}${env}${locationCode}' +} + +var storage = { + name: format(metadata.shortName, 'st') + location: location + sku: storageAccountSku +} + +resource st 'Microsoft.Storage/storageAccounts@2019-06-01' = { + name: storage.name + location: storage.location + kind: 'StorageV2' + sku: { + name: storage.sku + } + properties: { + supportsHttpsTrafficOnly: true + } +} + +var eventgrid = { + name: format(metadata.longName, 'evtgrd', '-topic') + location: location + inputSchema: eventGridInputSchema + outputSchema: eventGridOutputSchema +} + +resource evtgrdTopic 'Microsoft.EventGrid/topics@2020-06-01' = { + name: eventgrid.name + location: eventgrid.location + properties: { + inputSchema: eventgrid.inputSchema + publicNetworkAccess: 'Enabled' + } +} + +var workspace = { + name: format(metadata.longName, 'wrkspc', '') + location: location +} + +resource wrkspc 'Microsoft.OperationalInsights/workspaces@2020-08-01' = { + name: workspace.name + location: workspace.location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + workspaceCapping: { + dailyQuotaGb: -1 + } + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +var appInsights = { + name: format(metadata.longName, 'appins', '') + location: location +} + +resource appins 'Microsoft.Insights/components@2020-02-02-preview' = { + name: appInsights.name + location: appInsights.location + kind: 'web' + properties: { + Flow_Type: 'Bluefield' + Application_Type: 'web' + Request_Source: 'rest' + WorkspaceResourceId: wrkspc.id + } +} + +var servicePlan = { + name: format(metadata.longName, 'csplan', '') + location: location +} + +resource csplan 'Microsoft.Web/serverfarms@2020-06-01' = { + name: servicePlan.name + location: servicePlan.location + kind: 'functionApp' + sku: { + name: 'Y1' + tier: 'Dynamic' + } +} + +var functionApp = { + name: format(metadata.longName, 'fncapp', '') + location: location + environment: functionAppEnvironment + runtime: functionAppWorkerRuntime + timezone: functionAppTimezone +} + +var websub = { + subscriptionUri: websubSubscriptionUri + callbackEndpoint: websubCallbackEndpoint +} + +resource fncapp 'Microsoft.Web/sites@2020-06-01' = { + name: functionApp.name + location: functionApp.location + kind: 'functionapp' + properties: { + serverFarmId: csplan.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'APPINSIGHTS_INSTRUMENTATIONKEY' + value: reference(appins.id, '2020-02-02-preview', 'Full').properties.InstrumentationKey + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: reference(appins.id, '2020-02-02-preview', 'Full').properties.connectionString + } + { + name: 'AZURE_FUNCTIONS_ENVIRONMENT' + value: functionApp.environment + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${st.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(st.id, '2019-06-01').keys[0].value}' + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~3' + } + { + name: 'FUNCTION_APP_EDIT_MODE' + value: 'readonly' + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: functionApp.runtime + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${st.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(st.id, '2019-06-01').keys[0].value}' + } + { + name: 'WEBSITE_CONTENTSHARE' + value: functionApp.name + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~12' + } + { + name: 'WEBSITE_TIME_ZONE' + value: functionApp.timezone + } + // WebSub specific settings + { + name: 'WebSub__SubscriptionUri' + value: websub.subscriptionUri + } + { + name: 'WebSub__CallbackUri' + value: 'https://${functionApp.name}.azurewebsites.net/${websub.callbackEndpoint}' + } + { + name: 'EventGrid__Topic__Endpoint' + value: reference(evtgrdTopic.id, '2020-06-01', 'Full').properties.endpoint + } + { + name: 'EventGrid__Topic__AccessKey' + value: listKeys(evtgrdTopic.id, '2020-06-01').key1 + } + ] + } + } +} diff --git a/resources/azuredeploy.json b/resources/azuredeploy.json new file mode 100644 index 0000000..0a865be --- /dev/null +++ b/resources/azuredeploy.json @@ -0,0 +1,264 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string" + }, + "env": { + "type": "string", + "defaultValue": "dev", + "allowedValues": [ + "dev", + "test", + "prod" + ] + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "locationCode": { + "type": "string", + "defaultValue": "krc" + }, + "storageAccountSku": { + "type": "string", + "defaultValue": "Standard_LRS" + }, + "eventGridInputSchema": { + "type": "string", + "defaultValue": "CloudEventSchemaV1_0", + "allowedValues": [ + "CloudEventSchemaV1_0", + "CustomEventSchema", + "EventGridSchema" + ] + }, + "eventGridOutputSchema": { + "type": "string", + "defaultValue": "CloudEventSchemaV1_0", + "allowedValues": [ + "CloudEventSchemaV1_0", + "CustomInputSchema", + "EventGridSchema" + ] + }, + "functionAppWorkerRuntime": { + "type": "string", + "defaultValue": "dotnet" + }, + "functionAppEnvironment": { + "type": "string", + "defaultValue": "Development", + "allowedValues": [ + "Development", + "Staging", + "Production" + ] + }, + "functionAppTimezone": { + "type": "string", + "defaultValue": "Korea Standard Time" + }, + "websubSubscriptionUri": { + "type": "string", + "defaultValue": "https://pubsubhubbub.appspot.com/subscribe" + }, + "websubCallbackEndpoint": { + "type": "string", + "defaultValue": "api/callback" + } + }, + "functions": [], + "variables": { + "metadata": { + "longName": "[format('{{0}}-{0}{{1}}-{1}-{2}', parameters('name'), parameters('env'), parameters('locationCode'))]", + "shortName": "[format('{{0}}{0}{1}{2}', parameters('name'), parameters('env'), parameters('locationCode'))]" + }, + "storage": { + "name": "[format(variables('metadata').shortName, 'st')]", + "location": "[parameters('location')]", + "sku": "[parameters('storageAccountSku')]" + }, + "eventgrid": { + "name": "[format(variables('metadata').longName, 'evtgrd', '-topic')]", + "location": "[parameters('location')]", + "inputSchema": "[parameters('eventGridInputSchema')]", + "outputSchema": "[parameters('eventGridOutputSchema')]" + }, + "workspace": { + "name": "[format(variables('metadata').longName, 'wrkspc', '')]", + "location": "[parameters('location')]" + }, + "appInsights": { + "name": "[format(variables('metadata').longName, 'appins', '')]", + "location": "[parameters('location')]" + }, + "servicePlan": { + "name": "[format(variables('metadata').longName, 'csplan', '')]", + "location": "[parameters('location')]" + }, + "functionApp": { + "name": "[format(variables('metadata').longName, 'fncapp', '')]", + "location": "[parameters('location')]", + "environment": "[parameters('functionAppEnvironment')]", + "runtime": "[parameters('functionAppWorkerRuntime')]", + "timezone": "[parameters('functionAppTimezone')]" + }, + "websub": { + "subscriptionUri": "[parameters('websubSubscriptionUri')]", + "callbackEndpoint": "[parameters('websubCallbackEndpoint')]" + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2019-06-01", + "name": "[variables('storage').name]", + "location": "[variables('storage').location]", + "kind": "StorageV2", + "sku": { + "name": "[variables('storage').sku]" + }, + "properties": { + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.EventGrid/topics", + "apiVersion": "2020-06-01", + "name": "[variables('eventgrid').name]", + "location": "[variables('eventgrid').location]", + "properties": { + "inputSchema": "[variables('eventgrid').inputSchema]", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2020-08-01", + "name": "[variables('workspace').name]", + "location": "[variables('workspace').location]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30, + "workspaceCapping": { + "dailyQuotaGb": -1 + }, + "publicNetworkAccessForIngestion": "Enabled", + "publicNetworkAccessForQuery": "Enabled" + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02-preview", + "name": "[variables('appInsights').name]", + "location": "[variables('appInsights').location]", + "kind": "web", + "properties": { + "Flow_Type": "Bluefield", + "Application_Type": "web", + "Request_Source": "rest", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('workspace').name)]" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('workspace').name)]" + ] + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2020-06-01", + "name": "[variables('servicePlan').name]", + "location": "[variables('servicePlan').location]", + "kind": "functionApp", + "sku": { + "name": "Y1", + "tier": "Dynamic" + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2020-06-01", + "name": "[variables('functionApp').name]", + "location": "[variables('functionApp').location]", + "kind": "functionapp", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlan').name)]", + "httpsOnly": true, + "siteConfig": { + "appSettings": [ + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsights').name), '2020-02-02-preview', 'Full').properties.InstrumentationKey]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsights').name), '2020-02-02-preview', 'Full').properties.connectionString]" + }, + { + "name": "AZURE_FUNCTIONS_ENVIRONMENT", + "value": "[variables('functionApp').environment]" + }, + { + "name": "AzureWebJobsStorage", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storage').name, environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storage').name), '2019-06-01').keys[0].value)]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~3" + }, + { + "name": "FUNCTION_APP_EDIT_MODE", + "value": "readonly" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "[variables('functionApp').runtime]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storage').name, environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storage').name), '2019-06-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[variables('functionApp').name]" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "~12" + }, + { + "name": "WEBSITE_TIME_ZONE", + "value": "[variables('functionApp').timezone]" + }, + { + "name": "WebSub__SubscriptionUri", + "value": "[variables('websub').subscriptionUri]" + }, + { + "name": "WebSub__CallbackUri", + "value": "[format('https://{0}.azurewebsites.net/{1}', variables('functionApp').name, variables('websub').callbackEndpoint)]" + }, + { + "name": "EventGrid__Topic__Endpoint", + "value": "[reference(resourceId('Microsoft.EventGrid/topics', variables('eventgrid').name), '2020-06-01', 'Full').properties.endpoint]" + }, + { + "name": "EventGrid__Topic__AccessKey", + "value": "[listKeys(resourceId('Microsoft.EventGrid/topics', variables('eventgrid').name), '2020-06-01').key1]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', variables('appInsights').name)]", + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlan').name)]", + "[resourceId('Microsoft.EventGrid/topics', variables('eventgrid').name)]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storage').name)]" + ] + } + ] +} \ No newline at end of file diff --git a/resources/azuredeploy.parameters.json b/resources/azuredeploy.parameters.json new file mode 100644 index 0000000..c4c7464 --- /dev/null +++ b/resources/azuredeploy.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "value": "" + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.gitignore b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.gitignore new file mode 100644 index 0000000..3c3f4e6 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/extensions.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/extensions.json new file mode 100644 index 0000000..de991f4 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp" + ] +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/launch.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/launch.json new file mode 100644 index 0000000..894cbe6 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} \ No newline at end of file diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/settings.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/settings.json new file mode 100644 index 0000000..9977b0e --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "azureFunctions.deploySubpath": "bin/Release/netcoreapp3.1/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~3", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "publish" +} \ No newline at end of file diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/tasks.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/tasks.json new file mode 100644 index 0000000..b8a5159 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/.vscode/tasks.json @@ -0,0 +1,69 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "build", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "clean release", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release", + "problemMatcher": "$msCompile" + }, + { + "type": "func", + "dependsOn": "build", + "options": { + "cwd": "${workspaceFolder}/bin/Debug/netcoreapp3.1" + }, + "command": "host start --verbose", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" + } + ] +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/CallbackHttpTrigger.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/CallbackHttpTrigger.cs new file mode 100644 index 0000000..de0ba74 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/CallbackHttpTrigger.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Extensions; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; + +using Newtonsoft.Json; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp +{ + /// + /// This represents the HTTP trigger entity to handle callback requests. + /// + public class CallbackHttpTrigger + { + private readonly ICallbackService _service; + + /// + /// Initializes a new instance of the class. + /// + /// instance. + public CallbackHttpTrigger(ICallbackService service) + { + this._service = service ?? throw new ArgumentNullException(nameof(service)); + } + + /// + /// Invokes the callback request handler. + /// + /// instance. + /// instance. + /// instance. + /// Returns the instance. + [FunctionName(nameof(CallbackHttpTrigger.CallbackAsync))] + public async Task CallbackAsync( + [HttpTrigger(AuthorizationLevel.Function, HttpTriggerKeys.GetMethod, HttpTriggerKeys.PostMethod, Route = "callback")] HttpRequest req, + ExecutionContext context, + ILogger log) + { + var requestId = (string)req.HttpContext.Items["MS_AzureFunctionsRequestID"]; + var headers = req.Headers.ToDictionary(p => p.Key, p => string.Join("|", p.Value)); + + log.LogInformation($"WebSub subscription callback was invoked."); + log.LogInformation($"RequestID: {requestId}"); + log.LogInformation($"Callback Request Headers: {JsonConvert.SerializeObject(headers, Formatting.Indented)}"); + + var method = req.Method.ToUpperInvariant(); + + var verificationResult = await this._service.ProcessVerificationAsync(method, req.Query).ConfigureAwait(false); + if (verificationResult != null) + { + log.LogInformation($"Request verification for {verificationResult.Mode.ToValueString()} has {((int)verificationResult.StatusCode < 400 ? string.Empty : "NOT ")}been processed"); + + return new ObjectResult(verificationResult.Challenge) { StatusCode = (int)verificationResult.StatusCode }; + } + + var links = headers[CallbackKeys.LinkHeader] + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim().Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries)) + .ToDictionary(p => p.Last().Trim(), p => p.First().Trim().Replace("<", string.Empty).Replace(">", string.Empty)); + + var payload = default(string); + using (var reader = new StreamReader(req.Body)) + { + payload = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + var eventResult = await this._service.ProcessEventAsync(method, payload, links).ConfigureAwait(false); + if (eventResult == null) + { + log.LogInformation($"Event has NOT been processed"); + + return new StatusCodeResult((int)HttpStatusCode.BadRequest); + } + + log.LogInformation($"Event has {((int)eventResult.StatusCode < 400 ? string.Empty : "NOT ")}been processed"); + log.LogInformation($"Event Response Headers: {JsonConvert.SerializeObject(eventResult.Headers, Formatting.Indented)}"); + + return new StatusCodeResult((int)eventResult.StatusCode); + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/CallbackKeys.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/CallbackKeys.cs new file mode 100644 index 0000000..17894ad --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/CallbackKeys.cs @@ -0,0 +1,33 @@ +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants +{ + /// + /// This specifies the keys used for callbacks. + /// + public static class CallbackKeys + { + /// + /// Identifies 'rel=self'. + /// + public const string RelSelf = "rel=self"; + + /// + /// Identifies 'rel=hub'. + /// + public const string RelHub = "rel=hub"; + + /// + /// Identifies 'application/cloudevents+json'. + /// + public const string DataContentType = "application/cloudevents+json"; + + /// + /// Identifies '{at:deleted-entry'. + /// + public const string DeletedEntry = " + /// Identifies 'Link'. + /// + public const string LinkHeader = "Link"; + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/HttpTriggerKeys.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/HttpTriggerKeys.cs new file mode 100644 index 0000000..040f7b2 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/HttpTriggerKeys.cs @@ -0,0 +1,18 @@ +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants +{ + /// + /// This specifies the keys used for HTTP triggers. + /// + public static class HttpTriggerKeys + { + /// + /// Identifies 'GET'. + /// + public const string GetMethod = "GET"; + + /// + /// Identifies 'POST'. + /// + public const string PostMethod = "POST"; + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/SubscriptionKeys.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/SubscriptionKeys.cs new file mode 100644 index 0000000..b3ff282 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/SubscriptionKeys.cs @@ -0,0 +1,48 @@ +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants +{ + /// + /// This specifies the keys used for subscriptions. + /// + public static class SubscriptionKeys + { + /// + /// Identifies 'hub.callback'. + /// + public const string HubCallback = "hub.callback"; + + /// + /// Identifies 'hub.topic'. + /// + public const string HubTopic = "hub.topic"; + + /// + /// Identifies 'hub.verify'. + /// + public const string HubVerify = "hub.verify"; + + /// + /// Identifies 'hub.mode'. + /// + public const string HubMode = "hub.mode"; + + /// + /// Identifies 'hub.challenge'. + /// + public const string HubChallenge = "hub.challenge"; + + /// + /// Identifies 'hub.verify_token'. + /// + public const string HubVerifyToken = "hub.verify_token"; + + /// + /// Identifies 'hub.secret'. + /// + public const string HubSecret = "hub.secret"; + + /// + /// Identifies 'hub.lease_seconds'. + /// + public const string HubLeaseSeconds = "hub.lease_seconds"; + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Extensions/EnumExtensions.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..77b5066 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Extensions/EnumExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Extensions +{ + /// + /// This represents the extension entity for enums. + /// + public static class EnumExtensions + { + /// + /// Gets the display name of the enum value. + /// + /// Enum value. + /// Display name of the enum value. + public static string ToValueString(this Enum @enum) + { + var type = @enum.GetType(); + var member = type.GetMember(@enum.ToString()).First(); + var attribute = member.GetCustomAttribute(inherit: false); + var name = attribute == null ? @enum.ToString() : attribute.Value; + + return name; + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs new file mode 100644 index 0000000..f13193f --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs @@ -0,0 +1,63 @@ +using System; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the app settings entity. + /// + public class AppSettings + { + /// + /// Gets the instance. + /// + public virtual WebSubSettings WebSub { get; } = new WebSubSettings(); + + /// + /// Gets the instance. + /// + public virtual EventGridSettings EventGrid { get; } = new EventGridSettings(); + } + + /// + /// This represents the app settings entity for WebSub. + /// + public class WebSubSettings + { + /// + /// Gets the URI for subscription. + /// + public virtual string SubscriptionUri { get; } = Environment.GetEnvironmentVariable("WebSub__SubscriptionUri"); + + /// + /// Gets the URI for callback. + /// + public virtual string CallbackUri { get; } = Environment.GetEnvironmentVariable("WebSub__CallbackUri"); + } + + /// + /// This represents the app settings entity for EventGrid. + /// + public class EventGridSettings + { + /// + /// Gets the instance. + /// + public virtual EventGridTopicSettings Topic { get; } = new EventGridTopicSettings(); + } + + /// + /// This represents the app settings entity for EventGrid Topic. + /// + public class EventGridTopicSettings + { + /// + /// Gets the endpoint URI for EventGrid Topic. + /// + public virtual string Endpoint { get; } = Environment.GetEnvironmentVariable("EventGrid__Topic__Endpoint"); + + /// + /// Gets the access key to EventGrid Topic. + /// + public virtual string AccessKey { get; } = Environment.GetEnvironmentVariable("EventGrid__Topic__AccessKey"); + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/CallbackResponse.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/CallbackResponse.cs new file mode 100644 index 0000000..9ed54a8 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/CallbackResponse.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the response entity for callbacks. + /// + public class CallbackResponse : Response + { + /// + /// Gets or sets the list of headers from the callback response. + /// + /// Header key. + /// Heaver value. + public virtual Dictionary Headers { get; set; } = new Dictionary(); + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/Response.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/Response.cs new file mode 100644 index 0000000..3bcd0ac --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/Response.cs @@ -0,0 +1,15 @@ +using System.Net; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the response entity. This MUST be inherited. + /// + public abstract class Response + { + /// + /// Gets or sets the value. + /// + public virtual HttpStatusCode StatusCode { get; set; } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionMode.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionMode.cs new file mode 100644 index 0000000..f704750 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionMode.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This specifies the subscription mode. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum SubscriptionMode + { + /// + /// Identifies 'undefined'. + /// + [EnumMember(Value = "undefined")] + Undefined = 0, + + /// + /// Identifies 'unsubscribe'. + /// + [EnumMember(Value = "unsubscribe")] + Unsubscribe = 1, + + /// + /// Identifies 'subscribe'. + /// + [EnumMember(Value = "subscribe")] + Subscribe = 2 + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionRequest.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionRequest.cs new file mode 100644 index 0000000..7bc9e76 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionRequest.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the request entity for subscriptions. + /// + public class SubscriptionRequest + { + /// + /// Gets or sets the topic URI. + /// + [JsonProperty("topicUri")] + public virtual string TopicUri { get; set; } = "https://www.youtube.com/xml/feeds/videos.xml?channel_id="; + + /// + /// Gets or sets the subscription mode. + /// + [JsonProperty("mode")] + public virtual SubscriptionMode Mode { get; set; } = SubscriptionMode.Subscribe; + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionResponse.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionResponse.cs new file mode 100644 index 0000000..fb83343 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionResponse.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the response entity for subscriptions. + /// + public class SubscriptionResponse : Response + { + /// + /// Gets or sets the list of headers from the subscription response. + /// + /// Header key. + /// Heaver value. + public virtual Dictionary Headers { get; set; } = new Dictionary(); + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationQuery.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationQuery.cs new file mode 100644 index 0000000..7e85ade --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationQuery.cs @@ -0,0 +1,85 @@ +using System; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; + +using Microsoft.AspNetCore.Http; + +using Newtonsoft.Json; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the query entity to verify subscription requests. + /// + public class SubscriptionVerificationQuery + { + /// + /// Initializes a new instance of the class. + /// + public SubscriptionVerificationQuery() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// instance. + public SubscriptionVerificationQuery(IQueryCollection query) + { + this.HubTopic = GetTopicUri(query[SubscriptionKeys.HubTopic]); + this.HubChallenge = query[SubscriptionKeys.HubChallenge]; + this.HubMode = GetSubscriptionMode(query[SubscriptionKeys.HubMode]); + this.HubLeaseSeconds = GetLeaseSeconds(query[SubscriptionKeys.HubLeaseSeconds]); + } + + /// + /// Gets or sets the topic URI. + /// + [JsonProperty(SubscriptionKeys.HubTopic)] + public Uri HubTopic { get; set; } + + /// + /// Gets or sets the challenge value. + /// + [JsonProperty(SubscriptionKeys.HubChallenge)] + public string HubChallenge { get; set; } + + /// + /// Gets or sets the subscription mode value. + /// + [JsonProperty(SubscriptionKeys.HubMode)] + public SubscriptionMode HubMode { get; set; } + + /// + /// Gets or sets the expiration period in seconds. + /// + [JsonProperty(SubscriptionKeys.HubLeaseSeconds)] + public long HubLeaseSeconds { get; set; } + + private static Uri GetTopicUri(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException(nameof(value)); + } + + var topic = new Uri(value); + + return topic; + } + + private static SubscriptionMode GetSubscriptionMode(string value) + { + var mode = Enum.Parse(value, ignoreCase: true); + + return mode; + } + + private static long GetLeaseSeconds(string value) + { + var secs = string.IsNullOrWhiteSpace(value) ? long.MinValue : Convert.ToInt64(value); + + return secs; + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationType.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationType.cs new file mode 100644 index 0000000..13c3110 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/SubscriptionVerificationType.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This specifies the verification type. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum SubscriptionVerificationType + { + /// + /// Identifies 'undefined'. + /// + [EnumMember(Value = "undefined")] + Undefined = 0, + + /// + /// Identifies 'async'. + /// + [EnumMember(Value = "async")] + Asynchronous = 1, + + /// + /// Identifies 'sync'. + /// + [EnumMember(Value = "sync")] + Synchronous = 2 + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VerificationResponse.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VerificationResponse.cs new file mode 100644 index 0000000..4adb175 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VerificationResponse.cs @@ -0,0 +1,18 @@ +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the response entity for subscription request verification. + /// + public class VerificationResponse : Response + { + /// + /// Gets or sets the challenge code. + /// + public virtual string Challenge { get; set; } + + /// + /// Gets or sets the value. + /// + public virtual SubscriptionMode Mode { get; set; } = SubscriptionMode.Undefined; + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/YouTubeVideoEventType.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/YouTubeVideoEventType.cs new file mode 100644 index 0000000..f9badbd --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/YouTubeVideoEventType.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This specifies the YouTube video event type. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum YouTubeVideoEventType + { + /// + /// Identifies 'undefined'. + /// + [EnumMember(Value = "undefined")] + Undefined = 0, + + /// + /// Identifies 'published'. + /// + [EnumMember(Value = "com.youtube.video.unpublished")] + Unpublished = 1, + + /// + /// Identifies 'unpublished'. + /// + [EnumMember(Value = "com.youtube.video.published")] + Published = 2 + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs new file mode 100644 index 0000000..2478c2c --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +using Azure.Messaging.EventGrid; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Extensions; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; + +using Microsoft.AspNetCore.Http; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services +{ + /// + /// This represents the service entity for the subscription callbacks. + /// + public class CallbackService : ICallbackService + { + private readonly AppSettings _settings; + private readonly EventGridPublisherClient _publisher; + + /// + /// Initializes a new instance of the class. + /// + /// instance. + /// instance. + public CallbackService(AppSettings settings, EventGridPublisherClient publisher) + { + this._settings = settings ?? throw new ArgumentNullException(nameof(settings)); + this._publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); + } + + /// + public async Task ProcessVerificationAsync(string method, IQueryCollection query) + { + if (!HttpMethods.IsGet(method)) + { + return null; + } + + var queries = query.Any() ? new SubscriptionVerificationQuery(query) : null; + + var result = default(VerificationResponse); + if (!IsRequestValid(method, queries)) + { + result = new VerificationResponse() { StatusCode = HttpStatusCode.BadRequest, Mode = queries.HubMode }; + + return await Task.FromResult(result).ConfigureAwait(false); + } + + result = new VerificationResponse() { StatusCode = HttpStatusCode.OK, Mode = queries.HubMode, Challenge = queries.HubChallenge }; + + return await Task.FromResult(result).ConfigureAwait(false); + } + + /// + public async Task ProcessEventAsync(string method, string payload, Dictionary links) + { + if (!HttpMethods.IsPost(method)) + { + return null; + } + + if (string.IsNullOrWhiteSpace(payload)) + { + return null; + } + + var source = links[CallbackKeys.RelSelf]; + var type = GetEventType(payload).ToValueString(); + var dataContentType = CallbackKeys.DataContentType; + + var @event = new CloudEvent(source, type, payload, dataContentType); + var events = new List() { @event }; + + var result = default(CallbackResponse); + using (var response = await this._publisher.SendEventsAsync(events).ConfigureAwait(false)) + { + var headers = response.Headers.ToDictionary(p => p.Name, p => p.Value); + + result = new CallbackResponse() { StatusCode = (HttpStatusCode)response.Status, Headers = headers }; + } + + return result; + } + + private static bool IsRequestValid(string method, SubscriptionVerificationQuery queries) + { + return HttpMethods.IsGet(method) && queries != null && queries.HubMode != SubscriptionMode.Undefined && !string.IsNullOrWhiteSpace(queries.HubChallenge); + } + + private static YouTubeVideoEventType GetEventType(string payload) + { + var isEntryDeleted = payload.IndexOf(CallbackKeys.DeletedEntry) >= 0; + if (isEntryDeleted) + { + return YouTubeVideoEventType.Unpublished; + } + + return YouTubeVideoEventType.Published; + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ICallbackService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ICallbackService.cs new file mode 100644 index 0000000..6a31ed7 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ICallbackService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; + +using Microsoft.AspNetCore.Http; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services +{ + /// + /// This provides interfaces to class. + /// + public interface ICallbackService + { + /// + /// Processes the subscription request verification. + /// + /// HTTP request method. + /// Request querystring collection. + /// Returns the instance. + Task ProcessVerificationAsync(string method, IQueryCollection query); + + /// + /// Processes the event handling. + /// + /// HTTP request method. + /// Event payload. + /// WebSub links. + /// Returns the instance. + Task ProcessEventAsync(string method, string payload, Dictionary links); + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ISubscriptionService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ISubscriptionService.cs new file mode 100644 index 0000000..81a0074 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/ISubscriptionService.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services +{ + /// + /// This provides interfaces to class. + /// + public interface ISubscriptionService + { + /// + /// Processes the subscription request. + /// + /// instance. + /// Returns the instance. + Task ProcessSubscription(SubscriptionRequest req); + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs new file mode 100644 index 0000000..aec346c --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Extensions; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services +{ + /// + /// This represents the service entity for the subscription requests. + /// + public class SubscriptionService : ISubscriptionService + { + private readonly AppSettings _settings; + private readonly HttpClient _httpClient; + + /// + /// Initializes a new instance of the class. + /// + /// instance. + /// instance. + public SubscriptionService(AppSettings settings, HttpClient httpClient) + { + this._settings = settings ?? throw new ArgumentNullException(nameof(settings)); + this._httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + } + + /// + public async Task ProcessSubscription(SubscriptionRequest req) + { + var requestUri = new Uri(this._settings.WebSub.SubscriptionUri); + var values = new Dictionary() + { + { SubscriptionKeys.HubCallback, this._settings.WebSub.CallbackUri }, + { SubscriptionKeys.HubTopic, req.TopicUri }, + { SubscriptionKeys.HubVerify, SubscriptionVerificationType.Asynchronous.ToValueString() }, + { SubscriptionKeys.HubMode, req.Mode.ToValueString() }, + { SubscriptionKeys.HubVerifyToken, string.Empty }, + { SubscriptionKeys.HubSecret, string.Empty }, + { SubscriptionKeys.HubLeaseSeconds, string.Empty }, + }; + + var result = default(SubscriptionResponse); + using (var content = new FormUrlEncodedContent(values)) + using (var response = await this._httpClient.PostAsync(requestUri, content).ConfigureAwait(false)) + { + var headers = response.Headers.ToDictionary(p => p.Key, p => string.Join("|", p.Value)); + + result = new SubscriptionResponse() { StatusCode = response.StatusCode, Headers = headers }; + } + + return result; + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs new file mode 100644 index 0000000..d294b5c --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs @@ -0,0 +1,51 @@ +using System; + +using Azure; +using Azure.Messaging.EventGrid; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services; + +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +[assembly: FunctionsStartup(typeof(DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.StartUp))] +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp +{ + /// + /// This represents the entity to be invoked during the runtime startup. + /// + public class StartUp : FunctionsStartup + { + /// + public override void Configure(IFunctionsHostBuilder builder) + { + this.ConfigureAppSettings(builder.Services); + this.ConfigureClients(builder.Services); + this.ConfigureServices(builder.Services); + } + + private void ConfigureAppSettings(IServiceCollection services) + { + services.AddSingleton(); + } + + private void ConfigureClients(IServiceCollection services) + { + services.AddHttpClient(); + + var settings = services.BuildServiceProvider().GetService(); + var topicEndpoint = new Uri(settings.EventGrid.Topic.Endpoint); + var credential = new AzureKeyCredential(settings.EventGrid.Topic.AccessKey); + var publisher = new EventGridPublisherClient(topicEndpoint, credential); + + services.AddSingleton(publisher); + } + + private void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/SubscriptionHttpTrigger.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/SubscriptionHttpTrigger.cs new file mode 100644 index 0000000..d74cf51 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/SubscriptionHttpTrigger.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; + +using Newtonsoft.Json; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp +{ + /// + /// This represents the HTTP trigger entity to handle subscription requests. + /// + public class SubscriptionHttpTrigger + { + private readonly ISubscriptionService _service; + + /// + /// Initializes a new instance of the class. + /// + /// instance. + public SubscriptionHttpTrigger(ISubscriptionService service) + { + this._service = service ?? throw new ArgumentNullException(nameof(service)); + } + + /// + /// Invokes the subscription request handler. + /// + /// instance. + /// instance. + /// instance. + /// Returns the instance. + [FunctionName(nameof(SubscriptionHttpTrigger.SubscribeAsync))] + public async Task SubscribeAsync( + [HttpTrigger(AuthorizationLevel.Function, HttpTriggerKeys.PostMethod, Route = "subscribe")] HttpRequest req, + ExecutionContext context, + ILogger log) + { + var requestId = (string)req.HttpContext.Items["MS_AzureFunctionsRequestID"]; + var headers = req.Headers.ToDictionary(p => p.Key, p => string.Join("|", p.Value)); + + log.LogInformation($"WebSub subscription request was invoked."); + log.LogInformation($"RequestID: {requestId}"); + log.LogInformation($"Subscription Request Headers: {JsonConvert.SerializeObject(headers, Formatting.Indented)}"); + + var payload = default(SubscriptionRequest); + using (var reader = new StreamReader(req.Body)) + { + var serialised = await reader.ReadToEndAsync().ConfigureAwait(false); + payload = JsonConvert.DeserializeObject(serialised); + } + + var response = await this._service.ProcessSubscription(payload).ConfigureAwait(false); + + log.LogInformation($"Subscription Response Headers: {JsonConvert.SerializeObject(response.Headers, Formatting.Indented)}"); + + return new StatusCodeResult((int)response.StatusCode); + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj new file mode 100644 index 0000000..5d2229c --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj @@ -0,0 +1,28 @@ + + + + netcoreapp3.1 + v3 + + DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp + DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/host.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/host.json new file mode 100644 index 0000000..cdb274f --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/host.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json new file mode 100644 index 0000000..c301710 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json @@ -0,0 +1,13 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + + "WebSub__SubscriptionUri": "https://pubsubhubbub.appspot.com/subscribe", + "WebSub__CallbackUri": "https://.azurewebsites.net/api/callback", + + "EventGrid__Topic__Endpoint": "https://.-1.eventgrid.azure.net/api/events", + "EventGrid__Topic__AccessKey": "" + } +} diff --git a/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/CallbackHttpTriggerTests.cs b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/CallbackHttpTriggerTests.cs new file mode 100644 index 0000000..a6d9c31 --- /dev/null +++ b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/CallbackHttpTriggerTests.cs @@ -0,0 +1,20 @@ +using System; + +using FluentAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Tests +{ + [TestClass] + public class CallbackHttpTriggerTests + { + [TestMethod] + public void Given_Null_Parameter_When_Initiated_Then_It_Should_Throw_Exception() + { + Action action = () => new CallbackHttpTrigger(null); + + action.Should().Throw(); + } + } +} diff --git a/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/SubscriptionHttpTriggerTests.cs b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/SubscriptionHttpTriggerTests.cs new file mode 100644 index 0000000..92535ae --- /dev/null +++ b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/SubscriptionHttpTriggerTests.cs @@ -0,0 +1,20 @@ +using System; + +using FluentAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Tests +{ + [TestClass] + public class SubscriptionHttpTriggerTests + { + [TestMethod] + public void Given_Null_Parameter_When_Initiated_Then_It_Should_Throw_Exception() + { + Action action = () => new SubscriptionHttpTrigger(null); + + action.Should().Throw(); + } + } +} diff --git a/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests.csproj b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests.csproj new file mode 100644 index 0000000..19cb5b2 --- /dev/null +++ b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests.csproj @@ -0,0 +1,28 @@ + + + + netcoreapp3.1 + + false + + DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Tests + DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Tests + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From d10036dd6f2ec279127831ba12e673795c4467ad Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 1 Jan 2021 20:25:38 +0900 Subject: [PATCH 02/21] Update subscription service --- .../Models/AppSettings.cs | 5 +++++ .../Services/SubscriptionService.cs | 2 +- .../local.settings.samples.json | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs index f13193f..c5fcf59 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs @@ -32,6 +32,11 @@ public class WebSubSettings /// Gets the URI for callback. /// public virtual string CallbackUri { get; } = Environment.GetEnvironmentVariable("WebSub__CallbackUri"); + + /// + /// Gets the API key for callback. + /// + public virtual string CallbackKey { get; } = Environment.GetEnvironmentVariable("WebSub__CallbackKey"); } /// diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs index aec346c..5990457 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs @@ -35,7 +35,7 @@ public async Task ProcessSubscription(SubscriptionRequest var requestUri = new Uri(this._settings.WebSub.SubscriptionUri); var values = new Dictionary() { - { SubscriptionKeys.HubCallback, this._settings.WebSub.CallbackUri }, + { SubscriptionKeys.HubCallback, $"{this._settings.WebSub.CallbackUri}?code={this._settings.WebSub.CallbackKey}" }, { SubscriptionKeys.HubTopic, req.TopicUri }, { SubscriptionKeys.HubVerify, SubscriptionVerificationType.Asynchronous.ToValueString() }, { SubscriptionKeys.HubMode, req.Mode.ToValueString() }, diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json index c301710..cbd10a4 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json @@ -6,6 +6,7 @@ "WebSub__SubscriptionUri": "https://pubsubhubbub.appspot.com/subscribe", "WebSub__CallbackUri": "https://.azurewebsites.net/api/callback", + "WebSub__CallbackKey": "", "EventGrid__Topic__Endpoint": "https://.-1.eventgrid.azure.net/api/events", "EventGrid__Topic__AccessKey": "" From 5c7381bffd62aad9015f38f59a731ffbc452eaf5 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 1 Jan 2021 20:26:02 +0900 Subject: [PATCH 03/21] Add Logic App for subscription request --- resources/azuredeploy.bicep | 2 +- resources/azuredeploy.json | 2 +- resources/logappdeploy.subscription.bicep | 134 ++++++++++++++++ resources/logappdeploy.subscription.json | 150 ++++++++++++++++++ .../logappdeploy.subscription.parameters.json | 12 ++ 5 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 resources/logappdeploy.subscription.bicep create mode 100644 resources/logappdeploy.subscription.json create mode 100644 resources/logappdeploy.subscription.parameters.json diff --git a/resources/azuredeploy.bicep b/resources/azuredeploy.bicep index ed3533a..0c06d05 100644 --- a/resources/azuredeploy.bicep +++ b/resources/azuredeploy.bicep @@ -55,7 +55,7 @@ param websubSubscriptionUri string = 'https://pubsubhubbub.appspot.com/subscribe param websubCallbackEndpoint string = 'api/callback' var metadata = { - longName: '{0}-${name}{1}-${env}-${locationCode}' + longName: '{0}-${name}-${env}-${locationCode}{1}' shortName: '{0}${name}${env}${locationCode}' } diff --git a/resources/azuredeploy.json b/resources/azuredeploy.json index 0a865be..dd69afc 100644 --- a/resources/azuredeploy.json +++ b/resources/azuredeploy.json @@ -73,7 +73,7 @@ "functions": [], "variables": { "metadata": { - "longName": "[format('{{0}}-{0}{{1}}-{1}-{2}', parameters('name'), parameters('env'), parameters('locationCode'))]", + "longName": "[format('{{0}}-{0}-{1}-{2}{{1}}', parameters('name'), parameters('env'), parameters('locationCode'))]", "shortName": "[format('{{0}}{0}{1}{2}', parameters('name'), parameters('env'), parameters('locationCode'))]" }, "storage": { diff --git a/resources/logappdeploy.subscription.bicep b/resources/logappdeploy.subscription.bicep new file mode 100644 index 0000000..9c29d4c --- /dev/null +++ b/resources/logappdeploy.subscription.bicep @@ -0,0 +1,134 @@ +// Resource name +param name string + +// Provisioning environment +param env string { + allowed: [ + 'dev' + 'test' + 'prod' + ] + default: 'dev' +} + +// Resource location +param location string = resourceGroup().location + +// Resource location code +param locationCode string = 'krc' + +// Logic Apps +param logicAppTimezone string = 'Korea Standard Time' +param logicAppSubscriptionTopicUri string +param logicAppSubscriptionMode string { + allowed: [ + 'subscribe' + 'unsubscribe' + ] + default: 'subscribe' +} + +// Function App +param functionName string = 'SubscribeAsync' + +var metadata = { + longName: '{0}-${name}-${env}-${locationCode}{1}' + shortName: '{0}${name}${env}${locationCode}' +} + +var logicApp = { + name: format(metadata.longName, 'logapp', '-subscription') + location: location + timezone: logicAppTimezone + topicUri: logicAppSubscriptionTopicUri + subscriptionMode: logicAppSubscriptionMode +} + +var functionApp = { + name: format(metadata.longName, 'fncapp', '') + functionResourceId: resourceId('Microsoft.Web/sites/functions', format(metadata.longName, 'fncapp', ''), functionName) +} + +resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { + name: logicApp.name + location: logicApp.location + properties: { + state: 'Enabled' + parameters: { + functionAppKey: { + value: listKeys(functionApp.functionResourceId, '2020-06-01').default + } + } + definition: { + '$schema': 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#' + contentVersion: '1.0.0.0' + parameters: { + timezone: { + type: 'string' + defaultValue: logicApp.timezone + } + topicUri: { + type: 'string' + defaultValue: logicApp.topicUri + } + subscriptionMode: { + type: 'string' + defaultValue: logicApp.subscriptionMode + } + functionAppName: { + type: 'string' + defaultValue: functionApp.name + } + functionAppKey: { + type: 'string' + defaultValue: '' + } + } + triggers: { + Run_Daily_Scheduled_Request: { + type: 'Recurrence' + recurrence: { + frequency: 'Day' + interval: 1 + schedule: { + hours: [ + '1' + ] + minutes: [ + 0 + ] + } + timeZone: '@parameters(\'timezone\')' + } + } + } + actions: { + Build_Request_Payload: { + type: 'Compose' + runAfter: {} + inputs: { + topicUri: '@parameters(\'topicUri\')' + mode: '@parameters(\'subscriptionMode\')' + } + } + Send_Subscription_Request: { + type: 'Http' + runAfter: { + Build_Request_Payload: [ + 'Succeeded' + ] + } + inputs: { + method: 'POST' + uri: 'https://@{parameters(\'functionAppName\')}.azurewebsites.net/api/subscribe' + body: '@outputs(\'Build_Request_Payload\')' + headers: { + 'x-functions-key': '@parameters(\'functionAppKey\')' + } + } + } + } + outputs: {} + } + } +} diff --git a/resources/logappdeploy.subscription.json b/resources/logappdeploy.subscription.json new file mode 100644 index 0000000..30debf4 --- /dev/null +++ b/resources/logappdeploy.subscription.json @@ -0,0 +1,150 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string" + }, + "env": { + "type": "string", + "defaultValue": "dev", + "allowedValues": [ + "dev", + "test", + "prod" + ] + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "locationCode": { + "type": "string", + "defaultValue": "krc" + }, + "logicAppTimezone": { + "type": "string", + "defaultValue": "Korea Standard Time" + }, + "logicAppSubscriptionTopicUri": { + "type": "string" + }, + "logicAppSubscriptionMode": { + "type": "string", + "defaultValue": "subscribe", + "allowedValues": [ + "subscribe", + "unsubscribe" + ] + }, + "functionName": { + "type": "string", + "defaultValue": "SubscribeAsync" + } + }, + "functions": [], + "variables": { + "metadata": { + "longName": "[format('{{0}}-{0}-{1}-{2}{{1}}', parameters('name'), parameters('env'), parameters('locationCode'))]", + "shortName": "[format('{{0}}{0}{1}{2}', parameters('name'), parameters('env'), parameters('locationCode'))]" + }, + "logicApp": { + "name": "[format(variables('metadata').longName, 'logapp', '-subscription')]", + "location": "[parameters('location')]", + "timezone": "[parameters('logicAppTimezone')]", + "topicUri": "[parameters('logicAppSubscriptionTopicUri')]", + "subscriptionMode": "[parameters('logicAppSubscriptionMode')]" + }, + "functionApp": { + "name": "[format(variables('metadata').longName, 'fncapp', '')]", + "functionResourceId": "[resourceId('Microsoft.Web/sites/functions', format(variables('metadata').longName, 'fncapp', ''), parameters('functionName'))]" + } + }, + "resources": [ + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2019-05-01", + "name": "[variables('logicApp').name]", + "location": "[variables('logicApp').location]", + "properties": { + "state": "Enabled", + "parameters": { + "functionAppKey": { + "value": "[listKeys(variables('functionApp').functionResourceId, '2020-06-01').default]" + } + }, + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "timezone": { + "type": "string", + "defaultValue": "[variables('logicApp').timezone]" + }, + "topicUri": { + "type": "string", + "defaultValue": "[variables('logicApp').topicUri]" + }, + "subscriptionMode": { + "type": "string", + "defaultValue": "[variables('logicApp').subscriptionMode]" + }, + "functionAppName": { + "type": "string", + "defaultValue": "[variables('functionApp').name]" + }, + "functionAppKey": { + "type": "string", + "defaultValue": "" + } + }, + "triggers": { + "Run_Daily_Scheduled_Request": { + "type": "Recurrence", + "recurrence": { + "frequency": "Day", + "interval": 1, + "schedule": { + "hours": [ + "1" + ], + "minutes": [ + 0 + ] + }, + "timeZone": "@parameters('timezone')" + } + } + }, + "actions": { + "Build_Request_Payload": { + "type": "Compose", + "runAfter": {}, + "inputs": { + "topicUri": "@parameters('topicUri')", + "mode": "@parameters('subscriptionMode')" + } + }, + "Send_Subscription_Request": { + "type": "Http", + "runAfter": { + "Build_Request_Payload": [ + "Succeeded" + ] + }, + "inputs": { + "method": "POST", + "uri": "https://@{parameters('functionAppName')}.azurewebsites.net/api/subscribe", + "body": "@outputs('Build_Request_Payload')", + "headers": { + "x-functions-key": "@parameters('functionAppKey')" + } + } + } + }, + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/resources/logappdeploy.subscription.parameters.json b/resources/logappdeploy.subscription.parameters.json new file mode 100644 index 0000000..dce7063 --- /dev/null +++ b/resources/logappdeploy.subscription.parameters.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "value": "" + }, + "logicAppSubscriptionTopicUri": { + "value": "https://www.youtube.com/xml/feeds/videos.xml?channel_id=" + } + } +} From 88d3c3c3c60cfccd36057aa7e07f9c83d6836dae Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 1 Jan 2021 20:26:13 +0900 Subject: [PATCH 04/21] Add GitHub Actions workflow --- .github/workflows/dev.yaml | 144 +++++++++++++++++ .github/workflows/feature.yaml | 117 ++++++++++++++ .github/workflows/pr.yaml | 67 ++++++++ .github/workflows/release.yaml | 281 +++++++++++++++++++++++++++++++++ 4 files changed, 609 insertions(+) create mode 100644 .github/workflows/dev.yaml create mode 100644 .github/workflows/feature.yaml create mode 100644 .github/workflows/pr.yaml create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml new file mode 100644 index 0000000..08bb8f3 --- /dev/null +++ b/.github/workflows/dev.yaml @@ -0,0 +1,144 @@ +name: Build, Test & Deploy + +on: + push: + branches: + - dev + +env: + FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' + +jobs: + arm_template_build_test_deploy_dev: + name: 'DEV: ARM Templates Build, Test & Deploy' + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Run ARM TTK + uses: aliencube/arm-ttk-actions@v0.3 + id: armtest + with: + path: ./resources + + - name: Show ARM TTK test result + shell: bash + continue-on-error: true + run: | + echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' + + - name: Deploy ARM templates + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/azuredeploy.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} + + functionapp_build_test_deploy_dev: + name: 'DEV: FunctionApp Build, Test & Deploy' + needs: + - arm_template_build_test_deploy_dev + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - name: Restore NuGet packages + shell: bash + run: | + dotnet restore . + + - name: Build solution + shell: bash + run: | + dotnet build . -c Release + + - name: Test solution + shell: bash + run: | + dotnet test . -c Release + + - name: Create FunctionApp artifact + shell: bash + run: | + dotnet publish ${{ env.FUNCTIONAPP_PATH }} -c Release -o published + + - name: Get FunctionApp publish profile + id: publishprofile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_DEV }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + + - name: Deploy FunctionApp + uses: Azure/functions-action@v1 + with: + app-name: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + package: published + publish-profile: ${{ steps.publishprofile.outputs.profile }} + + - name: Reset FunctionApp publish profile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_DEV }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + reset: true + + - name: Update FunctionApp settings + uses: azure/CLI@v1 + with: + inlineScript: | + az functionapp config appsettings set \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --settings WebSub__CallbackKey=$(az functionapp function keys list \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ + --query "default" \ + -o tsv) + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Deploy ARM templates + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.subscription.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml new file mode 100644 index 0000000..0563413 --- /dev/null +++ b/.github/workflows/feature.yaml @@ -0,0 +1,117 @@ +name: Build, Test & Deploy + +on: + push: + branches: + - feature/* + - hotfix/* + +env: + FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' + +jobs: + arm_template_build_test: + name: 'WIP: ARM Templates Build & Test' + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Run ARM TTK + uses: aliencube/arm-ttk-actions@v0.3 + id: armtest + with: + path: ./resources + + - name: Show ARM TTK test result + shell: bash + continue-on-error: true + run: | + echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' + + functionapp_build_test_deploy_dev: + name: 'WIP: FunctionApp Build & Test' + needs: + - arm_template_build_test + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - name: Restore NuGet packages + shell: bash + run: | + dotnet restore . + + - name: Build solution + shell: bash + run: | + dotnet build . -c Release + + - name: Test solution + shell: bash + run: | + dotnet test . -c Release + + - name: Create FunctionApp artifact + shell: bash + run: | + dotnet publish ${{ env.FUNCTIONAPP_PATH }} -c Release -o published + + - name: Get FunctionApp publish profile + id: publishprofile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_DEV }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + + - name: Deploy FunctionApp + uses: Azure/functions-action@v1 + with: + app-name: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + package: published + publish-profile: ${{ steps.publishprofile.outputs.profile }} + + - name: Reset FunctionApp publish profile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_DEV }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + reset: true + + - name: Update FunctionApp settings + uses: azure/CLI@v1 + with: + inlineScript: | + az functionapp config appsettings set \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --settings WebSub__CallbackKey=$(az functionapp function keys list \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ + --query "default" \ + -o tsv) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000..cd533c7 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,67 @@ +name: Build & Test + +on: + pull_request: + branches: + - dev + +env: + FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' + +jobs: + arm_template_build_test: + name: 'PR: ARM Templates Build & Test' + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Run ARM TTK + uses: aliencube/arm-ttk-actions@v0.3 + id: armtest + with: + path: ./resources + + - name: Show ARM TTK test result + shell: bash + continue-on-error: true + run: | + echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' + + functionapp_build_test: + name: 'PR: FunctionApp Build & Test' + needs: + - arm_template_build_test + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - name: Restore NuGet packages + shell: bash + run: | + dotnet restore . + + - name: Build solution + shell: bash + run: | + dotnet build . -c Release + + - name: Test solution + shell: bash + run: | + dotnet test . -c Release diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..c173553 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,281 @@ +name: Build, Test & Deploy + +on: + push: + branches: + - release/* + +env: + FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' + +jobs: + arm_template_build_test_deploy_dev: + name: 'DEV: ARM Templates Build, Test & Deploy' + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Run ARM TTK + uses: aliencube/arm-ttk-actions@v0.3 + id: armtest + with: + path: ./resources + + - name: Show ARM TTK test result + shell: bash + continue-on-error: true + run: | + echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' + + - name: Deploy ARM templates + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'dvrl.kr' + template: 'resources/azuredeploy.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} + + arm_template_build_test_deploy_prod: + name: 'PROD: ARM Templates Build, Test & Deploy' + needs: + - arm_template_build_test_deploy_dev + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_PROD }} + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Run ARM TTK + uses: aliencube/arm-ttk-actions@v0.3 + id: armtest + with: + path: ./resources + + - name: Show ARM TTK test result + shell: bash + continue-on-error: true + run: | + echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' + + - name: Deploy ARM templates + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} + deploymentName: 'dvrl.kr' + template: 'resources/azuredeploy.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_PROD }} + + functionapp_build_test_deploy_dev: + name: 'DEV: FunctionApp Build, Test & Deploy' + needs: + - arm_template_build_test_deploy_dev + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - name: Restore NuGet packages + shell: bash + run: | + dotnet restore . + + - name: Build solution + shell: bash + run: | + dotnet build . -c Release + + - name: Test solution + shell: bash + run: | + dotnet test . -c Release + + - name: Create FunctionApp artifact + shell: bash + run: | + dotnet publish ${{ env.FUNCTIONAPP_PATH }} -c Release -o published + + - name: Get FunctionApp publish profile + id: publishprofile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_DEV }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + + - name: Deploy FunctionApp + uses: Azure/functions-action@v1 + with: + app-name: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + package: published + publish-profile: ${{ steps.publishprofile.outputs.profile }} + + - name: Reset FunctionApp publish profile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_DEV }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} + reset: true + + - name: Update FunctionApp settings + uses: azure/CLI@v1 + with: + inlineScript: | + az functionapp config appsettings set \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --settings WebSub__CallbackKey=$(az functionapp function keys list \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ + --query "default" \ + -o tsv) + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Deploy ARM templates + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.subscription.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + + functionapp_build_test_deploy_prod: + name: 'PROD: FunctionApp Build, Test & Deploy' + needs: + - arm_template_build_test_deploy_prod + - functionapp_build_test_deploy_dev + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_PROD }} + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - name: Restore NuGet packages + shell: bash + run: | + dotnet restore . + + - name: Build solution + shell: bash + run: | + dotnet build . -c Release + + - name: Test solution + shell: bash + run: | + dotnet test . -c Release + + - name: Create FunctionApp artifact + shell: bash + run: | + dotnet publish ${{ env.FUNCTIONAPP_PATH }} -c Release -o published + + - name: Get FunctionApp publish profile + id: publishprofile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_PROD }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_PROD }} + + - name: Deploy FunctionApp + uses: Azure/functions-action@v1 + with: + app-name: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_PROD }} + package: published + publish-profile: ${{ steps.publishprofile.outputs.profile }} + + - name: Reset FunctionApp publish profile + uses: aliencube/publish-profile-actions@v1 + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS_PROD }} + with: + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} + appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_PROD }} + reset: true + + - name: Update FunctionApp settings + uses: azure/CLI@v1 + with: + inlineScript: | + az functionapp config appsettings set \ + -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_PROD }} \ + --settings WebSub__CallbackKey=$(az functionapp function keys list \ + -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_PROD }} \ + --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ + --query "default" \ + -o tsv) + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Deploy ARM templates + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.subscription.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} From 16eeb5cf0aa33da4e36fcd2d2c6a524e762763e9 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 1 Jan 2021 21:52:55 +0900 Subject: [PATCH 05/21] Update workflow --- .github/workflows/dev.yaml | 2 +- .github/workflows/feature.yaml | 14 ++++++++++++++ .github/workflows/release.yaml | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 08bb8f3..200f91c 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -141,4 +141,4 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.subscription.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 0563413..8e145eb 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -115,3 +115,17 @@ jobs: --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ --query "default" \ -o tsv) + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Deploy ARM templates + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.subscription.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c173553..0efc7ac 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -183,7 +183,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.subscription.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} functionapp_build_test_deploy_prod: name: 'PROD: FunctionApp Build, Test & Deploy' @@ -278,4 +278,4 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.subscription.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_PROD }} From 2ed58caed6954362f69dd47e42e2db8632492cf1 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 1 Jan 2021 22:04:59 +0900 Subject: [PATCH 06/21] Update workflow --- .github/workflows/dev.yaml | 4 ++-- .github/workflows/feature.yaml | 4 ++-- .github/workflows/release.yaml | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 200f91c..f760917 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -119,7 +119,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | - az functionapp config appsettings set \ + settings=$(az functionapp config appsettings set \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ --settings WebSub__CallbackKey=$(az functionapp function keys list \ @@ -127,7 +127,7 @@ jobs: -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ --query "default" \ - -o tsv) + -o tsv)) - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 8e145eb..1ca90ec 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -106,7 +106,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | - az functionapp config appsettings set \ + settings=$(az functionapp config appsettings set \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ --settings WebSub__CallbackKey=$(az functionapp function keys list \ @@ -114,7 +114,7 @@ jobs: -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ --query "default" \ - -o tsv) + -o tsv)) - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0efc7ac..d455005 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -161,7 +161,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | - az functionapp config appsettings set \ + settings=$(az functionapp config appsettings set \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ --settings WebSub__CallbackKey=$(az functionapp function keys list \ @@ -169,7 +169,7 @@ jobs: -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ --query "default" \ - -o tsv) + -o tsv)) - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 @@ -256,7 +256,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | - az functionapp config appsettings set \ + settings=$(az functionapp config appsettings set \ -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_PROD }} \ --settings WebSub__CallbackKey=$(az functionapp function keys list \ @@ -264,7 +264,7 @@ jobs: -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_PROD }} \ --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ --query "default" \ - -o tsv) + -o tsv)) - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 From a33a5e32c8fbdef37b37ad8275992509641c1541 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 1 Jan 2021 23:03:22 +0900 Subject: [PATCH 07/21] Update README.md --- LICENSE | 2 +- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 90f1e0e..710dace 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 한국 DevRel 커뮤니티 +Copyright (c) 2020 DevRel Korea (한국 DevRel 커뮤니티) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 2cc6594..ab42062 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ -# youtube-websub-subscription-handler -This is a WebSub subscription handler for YouTube video feed update +# YouTube WebSub Subscription Handler # + +This is a WebSub subscription handler for YouTube video feed update. + + +## Readings ## + +* 한국어: TBD +* English: TBD + + +## Getting Started ## + +### Provisioning Resources ### + +TBD + + +### Deploying Azure Functions App ### + +TBD + + +### Deploying Azure Logic App for Scheduled Subscription ### + +TBD + + +### Deploying Azure Logic App as Event Handler ### + +TBD + + +## Contribution ## + +Your contributions are always welcome! All your work should be done in your forked repository. Once you finish your work with corresponding tests, please send us a pull request onto our `dev` branch for review. + + +## License ## + +**YouTube WebSub Subscription Handler** is released under [MIT License](http://opensource.org/licenses/MIT) + +> The MIT License (MIT) +> +> Copyright (c) 2020 DevRel Korea (한국 DevRel 커뮤니티) +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 6ce178ba07fa234fad6e2f92c23e36b77b15e230 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 11:59:08 +0900 Subject: [PATCH 08/21] Add a new endpoint to fetch YouTube item (#2) --- .../Constants/FetchKeys.cs | 23 ++++ .../Models/AppSettings.cs | 22 ++++ .../Models/FetchRequest.cs | 60 ++++++++++ .../Models/FetchResponse.cs | 50 +++++++++ .../Models/VideoItemDetails.cs | 35 ++++++ .../Services/CallbackService.cs | 9 ++ .../Services/FetchService.cs | 103 ++++++++++++++++++ .../Services/IFetchService.cs | 26 +++++ .../Services/SubscriptionService.cs | 19 +++- .../StartUp.cs | 14 ++- .../VideoDetailsHttpTrigger.cs | 63 +++++++++++ ...bSubSubscriptionHandler.FunctionApp.csproj | 2 + .../local.settings.samples.json | 5 +- 13 files changed, 424 insertions(+), 7 deletions(-) create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/FetchKeys.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchRequest.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VideoItemDetails.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/FetchService.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/IFetchService.cs create mode 100644 src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/FetchKeys.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/FetchKeys.cs new file mode 100644 index 0000000..8c521ec --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Constants/FetchKeys.cs @@ -0,0 +1,23 @@ +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants +{ + /// + /// This specifies the keys used to fetch YouTube video details. + /// + public static class FetchKeys + { + /// + /// Identifies 'videoId'. + /// + public const string VideoId = "videoId"; + + /// + /// Identifies 'channelId'. + /// + public const string ChannelId = "channelId"; + + /// + /// Identifies user-agent. + /// + public const string UserAgent = "YouTube-WebSub-Handler"; + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs index c5fcf59..6791fab 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/AppSettings.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models { @@ -16,6 +17,11 @@ public class AppSettings /// Gets the instance. /// public virtual EventGridSettings EventGrid { get; } = new EventGridSettings(); + + /// + /// Gets the instance. + /// + public virtual YouTubeSettings YouTube { get; } = new YouTubeSettings(); } /// @@ -65,4 +71,20 @@ public class EventGridTopicSettings /// public virtual string AccessKey { get; } = Environment.GetEnvironmentVariable("EventGrid__Topic__AccessKey"); } + + /// + /// This represents the app settings entity for YouTube API. + /// + public class YouTubeSettings + { + /// + /// Gets the API key. + /// + public virtual string ApiKey { get; } = Environment.GetEnvironmentVariable("YouTube__ApiKey"); + + /// + /// Gets the list of parts to fetch. + /// + public virtual IEnumerable FetchParts { get; } = Environment.GetEnvironmentVariable("YouTube__FetchParts").Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + } } diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchRequest.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchRequest.cs new file mode 100644 index 0000000..8b9172c --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchRequest.cs @@ -0,0 +1,60 @@ +using System; + +using Newtonsoft.Json; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the request entity for subscriptions. + /// + public class FetchRequest + { + /// + /// Gets or sets the Event ID. + /// + [JsonProperty("id")] + public virtual Guid Id { get; set; } + + /// + /// Gets or sets the CloudEvents spec version. This MUST be "1.0". + /// + [JsonProperty("specversion")] + public virtual string SpecVersion { get; set; } + + /// + /// Gets or sets the event source. This MUST be in the format of "https://www.youtube.com/xml/feeds/videos.xml?channel_id=[CHANNEL_ID]". + /// + [JsonProperty("source")] + public virtual string Source { get; set; } + + /// + /// Gets or sets the event type. This MUST be "com.youtube.video.published". + /// + [JsonProperty("type")] + public virtual string Type { get; set; } + + /// + /// Gets or sets the time when the event was published. + /// + [JsonProperty("time")] + public virtual DateTimeOffset Time { get; set; } + + /// + /// Gets or sets the event data content type. This MUST be "application/cloudevents+json". + /// + [JsonProperty("datacontenttype")] + public virtual string ContentType { get; set; } + + /// + /// Gets or sets the event data. This MUST be XML string. + /// + [JsonProperty("data")] + public virtual string Data { get; set; } + + /// + /// Gets or sets the trace parent value. + /// + [JsonProperty("traceparent")] + public virtual string TraceParent { get; set; } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs new file mode 100644 index 0000000..3f07f0b --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs @@ -0,0 +1,50 @@ +using System; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the response entity for YouTube video fetch details. + /// + public class FetchResponse + { + /// + /// Gets or sets the channel ID. + /// + public virtual string ChannelId { get; set; } + + /// + /// Gets or sets the video ID. + /// + public virtual string VideoId { get; set; } + + /// + /// Gets or sets the video title. + /// + public virtual string Title { get; set; } + + /// + /// Gets or sets the video description. + /// + public virtual string Description { get; set; } + + /// + /// Gets or sets the video link URL. + /// + public virtual Uri Link { get; set; } + + /// + /// Gets or sets the thumbnail link URL. + /// + public virtual Uri ThumbnailLink { get; set; } + + /// + /// Gets or sets the date of video published. + /// + public virtual DateTimeOffset DatePublished { get; set; } + + /// + /// Gets or sets the date of video updated. + /// + public virtual DateTimeOffset DateUpdated { get; set; } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VideoItemDetails.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VideoItemDetails.cs new file mode 100644 index 0000000..4e0e0cb --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/VideoItemDetails.cs @@ -0,0 +1,35 @@ +using System; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models +{ + /// + /// This represents the entity for YouTube video item details. + /// + public class VideoItemDetails + { + /// + /// Gets or sets the channel ID. + /// + public virtual string ChannelId { get; set; } + + /// + /// Gets or sets the video ID. + /// + public virtual string VideoId { get; set; } + + /// + /// Gets or sets the video link URL. + /// + public virtual Uri Link { get; set; } + + /// + /// Gets or sets the date of video published. + /// + public virtual DateTimeOffset DatePublished { get; set; } + + /// + /// Gets or sets the date of video updated. + /// + public virtual DateTimeOffset DateUpdated { get; set; } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs index 2478c2c..71407f7 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/CallbackService.cs @@ -17,6 +17,15 @@ namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services /// /// This represents the service entity for the subscription callbacks. /// + /// + /// This class implementation has the following external references: + /// + /// Docs: https://indieweb.org/How_to_publish_and_consume_WebSub + /// Docs: https://developers.google.com/youtube/v3/guides/push_notifications + /// Google PubSubHubbub: http://pubsubhubbub.appspot.com/ + /// Google PubShbHubbub Subscribe: https://pubsubhubbub.appspot.com/subscribe + /// + /// public class CallbackService : ICallbackService { private readonly AppSettings _settings; diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/FetchService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/FetchService.cs new file mode 100644 index 0000000..234bf66 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/FetchService.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using System.Linq; +using System.ServiceModel.Syndication; +using System.Threading.Tasks; +using System.Xml; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; + +using Google.Apis.Util; +using Google.Apis.YouTube.v3; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services +{ + /// + /// This represents the service entity to fetch YouTube video details. + /// + /// + /// This class implementation has the following external references: + /// + /// Docs: https://developers.google.com/youtube/v3/docs/videos + /// Developer Console: https://console.developers.google.com/apis/dashboard + /// NuGet Package: https://www.nuget.org/packages/Google.Apis.YouTube.v3 + /// GitHub Repository: https://github.com/googleapis/google-api-dotnet-client + /// + /// + public class FetchService : IFetchService + { + private readonly AppSettings _settings; + private readonly VideosResource _resource; + + /// + /// Initializes a new instance of the class. + /// + /// instance. + /// instance. + public FetchService(AppSettings settings, VideosResource resource) + { + this._settings = settings ?? throw new ArgumentNullException(nameof(settings)); + this._resource = resource ?? throw new ArgumentNullException(nameof(resource)); + } + + /// + public async Task ExtractVideoDetailsAsync(FetchRequest req) + { + var feed = await Task.Factory.StartNew(() => { + using (var xml = new StringReader(req.Data)) + using (var reader = XmlReader.Create(xml)) + { + return SyndicationFeed.Load(reader); + } + }).ConfigureAwait(false); + + var item = feed.Items.First(); + var videoId = item.ElementExtensions.FirstOrDefault(p => p.OuterName == FetchKeys.VideoId).GetObject(); + var channelId = item.ElementExtensions.FirstOrDefault(p => p.OuterName == FetchKeys.ChannelId).GetObject(); + var link = item.Links.First().Uri; + var published = item.PublishDate; + var updated = item.LastUpdatedTime; + + var details = new VideoItemDetails() + { + ChannelId = channelId, + VideoId = videoId, + Link = link, + DatePublished = published, + DateUpdated = updated, + }; + + return details; + } + + /// + public async Task FetchVideoDetailsAsync(VideoItemDetails item) + { + var part = new Repeatable(this._settings.YouTube.FetchParts); + var list = this._resource.List(part); + list.Id = item.VideoId; + + var result = await list.ExecuteAsync().ConfigureAwait(false); + + var snippet = result.Items[0].Snippet; + var title = snippet.Title; + var description = snippet.Description; + var thumbnail = snippet.Thumbnails.Maxres.Url; + + var response = new FetchResponse() + { + ChannelId = item.ChannelId, + VideoId = item.VideoId, + Title = title, + Description = description, + Link = item.Link, + ThumbnailLink = new Uri(thumbnail), + DatePublished = item.DatePublished, + DateUpdated = item.DateUpdated + }; + + return response; + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/IFetchService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/IFetchService.cs new file mode 100644 index 0000000..1edffb6 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/IFetchService.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services +{ + /// + /// This provides interfaces to class. + /// + public interface IFetchService + { + /// + /// Extracts the video details from the XML feed. + /// + /// instance. + /// Returns the instance. + Task ExtractVideoDetailsAsync(FetchRequest req); + + /// + /// Fetches the video details from YouTube API. + /// + /// instance. + /// Returns the instance. + Task FetchVideoDetailsAsync(VideoItemDetails item); + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs index 5990457..2f2937c 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Services/SubscriptionService.cs @@ -13,20 +13,29 @@ namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services /// /// This represents the service entity for the subscription requests. /// + /// + /// This class implementation has the following external references: + /// + /// Docs: https://indieweb.org/How_to_publish_and_consume_WebSub + /// Docs: https://developers.google.com/youtube/v3/guides/push_notifications + /// Google PubSubHubbub: http://pubsubhubbub.appspot.com/ + /// Google PubShbHubbub Subscribe: https://pubsubhubbub.appspot.com/subscribe + /// + /// public class SubscriptionService : ISubscriptionService { private readonly AppSettings _settings; - private readonly HttpClient _httpClient; + private readonly HttpClient _http; /// /// Initializes a new instance of the class. /// /// instance. - /// instance. - public SubscriptionService(AppSettings settings, HttpClient httpClient) + /// instance. + public SubscriptionService(AppSettings settings, HttpClient http) { this._settings = settings ?? throw new ArgumentNullException(nameof(settings)); - this._httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + this._http = http ?? throw new ArgumentNullException(nameof(http)); } /// @@ -46,7 +55,7 @@ public async Task ProcessSubscription(SubscriptionRequest var result = default(SubscriptionResponse); using (var content = new FormUrlEncodedContent(values)) - using (var response = await this._httpClient.PostAsync(requestUri, content).ConfigureAwait(false)) + using (var response = await this._http.PostAsync(requestUri, content).ConfigureAwait(false)) { var headers = response.Headers.ToDictionary(p => p.Key, p => string.Join("|", p.Value)); diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs index d294b5c..ea6e4cc 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/StartUp.cs @@ -3,9 +3,13 @@ using Azure; using Azure.Messaging.EventGrid; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services; +using Google.Apis.Services; +using Google.Apis.YouTube.v3; + using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; @@ -32,20 +36,28 @@ private void ConfigureAppSettings(IServiceCollection services) private void ConfigureClients(IServiceCollection services) { + var settings = services.BuildServiceProvider().GetService(); + services.AddHttpClient(); - var settings = services.BuildServiceProvider().GetService(); var topicEndpoint = new Uri(settings.EventGrid.Topic.Endpoint); var credential = new AzureKeyCredential(settings.EventGrid.Topic.AccessKey); var publisher = new EventGridPublisherClient(topicEndpoint, credential); services.AddSingleton(publisher); + + var initialiser = new BaseClientService.Initializer() { ApplicationName = FetchKeys.UserAgent, ApiKey = settings.YouTube.ApiKey }; + var youtube = new YouTubeService(initialiser); + var resource = new VideosResource(youtube); + + services.AddSingleton(resource); } private void ConfigureServices(IServiceCollection services) { services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } } diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs new file mode 100644 index 0000000..0aa2129 --- /dev/null +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Constants; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models; +using DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Services; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; + +using Newtonsoft.Json; + +namespace YouTubeWebSubSubscriptionHandler.FunctionApp +{ + /// + /// This represents the HTTP trigger entity to fetch YouTube video details. + /// + public class VideoDetailsHttpTrigger + { + private readonly IFetchService _service; + + /// + /// Initializes a new instance of the class. + /// + /// instance. + public VideoDetailsHttpTrigger(IFetchService service) + { + this._service = service ?? throw new ArgumentNullException(nameof(service)); + } + + /// + /// Invokes the fetch request handler. + /// + /// instance. + /// instance. + /// instance. + /// Returns the instance. + [FunctionName(nameof(VideoDetailsHttpTrigger.FetchAsync))] + public async Task FetchAsync( + [HttpTrigger(AuthorizationLevel.Function, HttpTriggerKeys.PostMethod, Route = "fetch")] HttpRequest req, + ExecutionContext context, + ILogger log) + { + log.LogInformation("C# HTTP trigger function processed a request."); + + var payload = default(FetchRequest); + using (var reader = new StreamReader(req.Body)) + { + var serialised = await reader.ReadToEndAsync().ConfigureAwait(false); + payload = JsonConvert.DeserializeObject(serialised); + } + + var item = await this._service.ExtractVideoDetailsAsync(payload).ConfigureAwait(false); + var response = await this._service.FetchVideoDetailsAsync(item).ConfigureAwait(false); + + return new OkObjectResult(response); + } + } +} diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj index 5d2229c..ac69a4d 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/YouTubeWebSubSubscriptionHandler.FunctionApp.csproj @@ -10,9 +10,11 @@ + + diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json index cbd10a4..226c733 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/local.settings.samples.json @@ -9,6 +9,9 @@ "WebSub__CallbackKey": "", "EventGrid__Topic__Endpoint": "https://.-1.eventgrid.azure.net/api/events", - "EventGrid__Topic__AccessKey": "" + "EventGrid__Topic__AccessKey": "", + + "YouTube__ApiKey": "", + "YouTube__FetchParts": "contentDetails,fileDetails,id,liveStreamingDetails,localizations,player,processingDetails,recordingDetails,snippet,statistics,status,suggestions,topicDetails" } } From c9eb803c9d8b7631d8d2c13600805c3c1cf9fe7b Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 12:53:20 +0900 Subject: [PATCH 09/21] Missing JSON serialisation (#3) --- .../Models/FetchResponse.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs index 3f07f0b..359097b 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/Models/FetchResponse.cs @@ -1,5 +1,7 @@ using System; +using Newtonsoft.Json; + namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Models { /// @@ -10,41 +12,49 @@ public class FetchResponse /// /// Gets or sets the channel ID. /// + [JsonProperty("channelId")] public virtual string ChannelId { get; set; } /// /// Gets or sets the video ID. /// + [JsonProperty("videoId")] public virtual string VideoId { get; set; } /// /// Gets or sets the video title. /// + [JsonProperty("title")] public virtual string Title { get; set; } /// /// Gets or sets the video description. /// + [JsonProperty("description")] public virtual string Description { get; set; } /// /// Gets or sets the video link URL. /// + [JsonProperty("link")] public virtual Uri Link { get; set; } /// /// Gets or sets the thumbnail link URL. /// + [JsonProperty("thumbnailLink")] public virtual Uri ThumbnailLink { get; set; } /// /// Gets or sets the date of video published. /// + [JsonProperty("datePublished")] public virtual DateTimeOffset DatePublished { get; set; } /// /// Gets or sets the date of video updated. /// + [JsonProperty("dateUpdated")] public virtual DateTimeOffset DateUpdated { get; set; } } } From 7924f897dcd44ad97c35b6d0156c08c64188156e Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 17:32:45 +0900 Subject: [PATCH 10/21] EventGrid Subscription Handler (#4) --- .github/workflows/dev.yaml | 2 +- .github/workflows/feature.yaml | 2 +- .github/workflows/release.yaml | 4 +- ...eploy.eventgridhandler-twitter.azpls.bicep | 275 +++++++++++++++++ ...deploy.eventgridhandler-twitter.azpls.json | 292 ++++++++++++++++++ ...tgridhandler-twitter.azpls.parameters.json | 9 + resources/logappdeploy.subscription.bicep | 2 +- resources/logappdeploy.subscription.json | 4 +- .../VideoDetailsHttpTrigger.cs | 2 +- .../VideoDetailsHttpTriggerTests.cs | 20 ++ 10 files changed, 604 insertions(+), 8 deletions(-) create mode 100644 resources/logappdeploy.eventgridhandler-twitter.azpls.bicep create mode 100644 resources/logappdeploy.eventgridhandler-twitter.azpls.json create mode 100644 resources/logappdeploy.eventgridhandler-twitter.azpls.parameters.json create mode 100644 test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index f760917..1c7a436 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -134,7 +134,7 @@ jobs: with: files: '**/*.bicep' - - name: Deploy ARM templates + - name: Deploy LogicApp for scheduled subscription to WebSub uses: Azure/arm-deploy@v1.0.1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 1ca90ec..50d3751 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -121,7 +121,7 @@ jobs: with: files: '**/*.bicep' - - name: Deploy ARM templates + - name: Deploy LogicApp for scheduled subscription to WebSub uses: Azure/arm-deploy@v1.0.1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d455005..0aef9e2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -176,7 +176,7 @@ jobs: with: files: '**/*.bicep' - - name: Deploy ARM templates + - name: Deploy LogicApp for scheduled subscription to WebSub uses: Azure/arm-deploy@v1.0.1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} @@ -271,7 +271,7 @@ jobs: with: files: '**/*.bicep' - - name: Deploy ARM templates + - name: Deploy LogicApp for scheduled subscription to WebSub uses: Azure/arm-deploy@v1.0.1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep b/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep new file mode 100644 index 0000000..6712105 --- /dev/null +++ b/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep @@ -0,0 +1,275 @@ +// Resource name +param name string + +// Provisioning environment +param env string { + allowed: [ + 'dev' + 'test' + 'prod' + ] + default: 'dev' +} + +// Resource location +param location string = resourceGroup().location + +// Resource location code +param locationCode string = 'krc' + +// Logic Apps +param logicAppAcceptedEventType string = 'com.youtube.video.published' +param logicAppAcceptedTitleSegment string = '애저듣보잡' + +// Function App +param functionName string = 'FetchAsync' + +// Twitter +param twitterApiKey string { + secure: true +} +param twitterApiSecret string { + secure: true +} + +var metadata = { + longName: '{0}-${name}-${env}-${locationCode}{1}' + shortName: '{0}${name}${env}${locationCode}' +} + +var twitterConnector = { + id: '${subscription().id}/providers/Microsoft.Web/locations/${location}/managedApis/twitter' + connectionId: '${resourceGroup().id}/providers/Microsoft.Web/connections/${format(metadata.longName, 'apicon', '-twitter-azpls')}' + connectionName: format(metadata.longName, 'apicon', '-twitter-azpls') + location: location + apiKey: twitterApiKey + apiSecret: twitterApiSecret +} + +resource apiconTwitter 'Microsoft.Web/connections@2016-06-01' = { + name: twitterConnector.connectionName + location: twitterConnector.location + kind: 'V1' + properties: { + displayName: twitterConnector.connectionName + api: { + id: twitterConnector.id + } + } +} + +var logicApp = { + name: format(metadata.longName, 'logapp', '-eventgrid-sub-handler-twitter') + location: location + acceptedEventType: logicAppAcceptedEventType + acceptedTitleSegment: logicAppAcceptedTitleSegment +} + +var functionApp = { + name: format(metadata.longName, 'fncapp', '') + functionResourceId: resourceId('Microsoft.Web/sites/functions', format(metadata.longName, 'fncapp', ''), functionName) +} + +resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { + name: logicApp.name + location: logicApp.location + properties: { + state: 'Enabled' + parameters: { + '$connections': { + value: { + twitter: { + id: twitterConnector.id + connectionId: twitterConnector.connectionId + connectionName: apiconTwitter.name + } + } + } + functionAppKey: { + value: listKeys(functionApp.functionResourceId, '2020-06-01').default + } + } + definition: { + '$schema': 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#' + contentVersion: '1.0.0.0' + parameters: { + '$connections': { + type: 'object' + defaultValue: {} + } + acceptedEventType: { + type: 'string' + defaultValue: logicApp.acceptedEventType + } + functionAppName: { + type: 'string' + defaultValue: functionApp.name + } + functionAppKey: { + type: 'string' + defaultValue: '' + } + acceptedTitleSegment: { + type: 'string' + defaultValue: logicApp.acceptedTitleSegment + } + } + triggers: { + manual: { + type: 'Request' + kind: 'Http' + inputs: { + schema: { + type: 'object' + properties: { + id: { + type: 'string' + } + specversion: { + type: 'string' + } + source: { + type: 'string' + } + type: { + type: 'string' + } + time: { + type: 'string' + } + datacontenttype: { + type: 'string' + } + data: { + type: 'string' + } + traceparent: { + type: 'string' + } + } + } + } + } + } + actions: { + Proceed_Only_If_Published: { + type: 'If' + runAfter: {} + expression: { + and: [ + { + equals: [ + '@triggerBody()?[\'type\']' + '@parameters(\'acceptedEventType\')' + ] + } + ] + } + actions: { + Fetch_YouTube_Video_Details: { + type: 'Http' + runAfter: {} + inputs: { + method: 'POST' + uri: 'https://@{parameters(\'functionAppName\')}.azurewebsites.net/api/fetch' + headers: { + 'x-functions-key': '@parameters(\'functionAppKey\')' + } + body: '@triggerBody()' + } + } + } + else: { + actions: { + Cancel_Processing_Event: { + type: 'Terminate' + runAfter: {} + inputs: { + runStatus: 'Cancelled' + } + } + } + } + } + Split_Title: { + type: 'Compose' + runAfter: { + 'Proceed_Only_If_Published': [ + 'Succeeded' + ] + } + inputs: '@split(body(\'Fetch_YouTube_Video_Details\')?[\'title\'], \'|\')' + } + Split_Description: { + type: 'Compose' + runAfter: { + 'Proceed_Only_If_Published': [ + 'Succeeded' + ] + } + inputs: '@split(body(\'Fetch_YouTube_Video_Details\')?[\'description\'], \'---\')' + } + Process_Only_If_Title_Met: { + type: 'If' + runAfter: { + Split_Title: [ + 'Succeeded' + ] + Split_Description: [ + 'Succeeded' + ] + } + expression: { + and: [ + { + equals: [ + '@trim(last(outputs(\'Split_Title\')))' + '@parameters(\'acceptedTitleSegment\')' + ] + } + ] + } + actions: { + Build_Tweet_Post: { + type: 'Compose' + runAfter: {} + inputs: '@{trim(first(outputs(\'Split_Description\')))}\n\n@{body(\'Fetch_YouTube_Video_Details\')?[\'link\']}' + } + } + else: { + actions: { + Cancel_Tweeting_Post: { + type: 'Terminate' + runAfter: {} + inputs: { + runStatus: 'Cancelled' + } + } + } + } + } + Post_Tweet: { + type: 'ApiConnection' + runAfter: { + Process_Only_If_Title_Met: [ + 'Succeeded' + ] + } + inputs: { + method: 'POST' + host: { + connection: { + name: '@parameters(\'$connections\')[\'twitter\'][\'connectionId\']' + } + } + path: '/posttweet' + queries: { + tweetText: '@{outputs(\'Build_Tweet_Post\')}' + } + } + } + } + outputs: {} + } + } +} diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.json b/resources/logappdeploy.eventgridhandler-twitter.azpls.json new file mode 100644 index 0000000..1c62295 --- /dev/null +++ b/resources/logappdeploy.eventgridhandler-twitter.azpls.json @@ -0,0 +1,292 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string" + }, + "env": { + "type": "string", + "defaultValue": "dev", + "allowedValues": [ + "dev", + "test", + "prod" + ] + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "locationCode": { + "type": "string", + "defaultValue": "krc" + }, + "logicAppAcceptedEventType": { + "type": "string", + "defaultValue": "com.youtube.video.published" + }, + "logicAppAcceptedTitleSegment": { + "type": "string", + "defaultValue": "애저듣보잡" + }, + "functionName": { + "type": "string", + "defaultValue": "FetchAsync" + }, + "twitterApiKey": { + "type": "secureString" + }, + "twitterApiSecret": { + "type": "secureString" + } + }, + "functions": [], + "variables": { + "metadata": { + "longName": "[format('{{0}}-{0}-{1}-{2}{{1}}', parameters('name'), parameters('env'), parameters('locationCode'))]", + "shortName": "[format('{{0}}{0}{1}{2}', parameters('name'), parameters('env'), parameters('locationCode'))]" + }, + "twitterConnector": { + "id": "[format('{0}/providers/Microsoft.Web/locations/{1}/managedApis/twitter', subscription().id, parameters('location'))]", + "connectionId": "[format('{0}/providers/Microsoft.Web/connections/{1}', resourceGroup().id, format(variables('metadata').longName, 'apicon', '-twitter-azpls'))]", + "connectionName": "[format(variables('metadata').longName, 'apicon', '-twitter-azpls')]", + "location": "[parameters('location')]", + "apiKey": "[parameters('twitterApiKey')]", + "apiSecret": "[parameters('twitterApiSecret')]" + }, + "logicApp": { + "name": "[format(variables('metadata').longName, 'logapp', '-eventgrid-sub-handler-twitter')]", + "location": "[parameters('location')]", + "acceptedEventType": "[parameters('logicAppAcceptedEventType')]", + "acceptedTitleSegment": "[parameters('logicAppAcceptedTitleSegment')]" + }, + "functionApp": { + "name": "[format(variables('metadata').longName, 'fncapp', '')]", + "functionResourceId": "[resourceId('Microsoft.Web/sites/functions', format(variables('metadata').longName, 'fncapp', ''), parameters('functionName'))]" + } + }, + "resources": [ + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('twitterConnector').connectionName]", + "location": "[variables('twitterConnector').location]", + "kind": "V1", + "properties": { + "displayName": "[variables('twitterConnector').connectionName]", + "api": { + "id": "[variables('twitterConnector').id]" + } + } + }, + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2019-05-01", + "name": "[variables('logicApp').name]", + "location": "[variables('logicApp').location]", + "properties": { + "state": "Enabled", + "parameters": { + "$connections": { + "value": { + "twitter": { + "id": "[variables('twitterConnector').id]", + "connectionId": "[variables('twitterConnector').connectionId]", + "connectionName": "[variables('twitterConnector').connectionName]" + } + } + }, + "functionAppKey": { + "value": "[listKeys(variables('functionApp').functionResourceId, '2020-06-01').default]" + } + }, + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { + "type": "object", + "defaultValue": {} + }, + "acceptedEventType": { + "type": "string", + "defaultValue": "[variables('logicApp').acceptedEventType]" + }, + "functionAppName": { + "type": "string", + "defaultValue": "[variables('functionApp').name]" + }, + "functionAppKey": { + "type": "string", + "defaultValue": "" + }, + "acceptedTitleSegment": { + "type": "string", + "defaultValue": "[variables('logicApp').acceptedTitleSegment]" + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "specversion": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "time": { + "type": "string" + }, + "datacontenttype": { + "type": "string" + }, + "data": { + "type": "string" + }, + "traceparent": { + "type": "string" + } + } + } + } + } + }, + "actions": { + "Proceed_Only_If_Published": { + "type": "If", + "runAfter": {}, + "expression": { + "and": [ + { + "equals": [ + "@triggerBody()?['type']", + "@parameters('acceptedEventType')" + ] + } + ] + }, + "actions": { + "Fetch_YouTube_Video_Details": { + "type": "Http", + "runAfter": {}, + "inputs": { + "method": "POST", + "uri": "https://@{parameters('functionAppName')}.azurewebsites.net/api/fetch", + "headers": { + "x-functions-key": "@parameters('functionAppKey')" + }, + "body": "@triggerBody()" + } + } + }, + "else": { + "actions": { + "Cancel_Processing_Event": { + "type": "Terminate", + "runAfter": {}, + "inputs": { + "runStatus": "Cancelled" + } + } + } + } + }, + "Split_Title": { + "type": "Compose", + "runAfter": { + "Proceed_Only_If_Published": [ + "Succeeded" + ] + }, + "inputs": "@split(body('Fetch_YouTube_Video_Details')?['title'], '|')" + }, + "Split_Description": { + "type": "Compose", + "runAfter": { + "Proceed_Only_If_Published": [ + "Succeeded" + ] + }, + "inputs": "@split(body('Fetch_YouTube_Video_Details')?['description'], '---')" + }, + "Process_Only_If_Title_Met": { + "type": "If", + "runAfter": { + "Split_Title": [ + "Succeeded" + ], + "Split_Description": [ + "Succeeded" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@trim(last(outputs('Split_Title')))", + "@parameters('acceptedTitleSegment')" + ] + } + ] + }, + "actions": { + "Build_Tweet_Post": { + "type": "Compose", + "runAfter": {}, + "inputs": "@{trim(first(outputs('Split_Description')))}\n\n@{body('Fetch_YouTube_Video_Details')?['link']}" + } + }, + "else": { + "actions": { + "Cancel_Tweeting_Post": { + "type": "Terminate", + "runAfter": {}, + "inputs": { + "runStatus": "Cancelled" + } + } + } + } + }, + "Post_Tweet": { + "type": "ApiConnection", + "runAfter": { + "Process_Only_If_Title_Met": [ + "Succeeded" + ] + }, + "inputs": { + "method": "POST", + "host": { + "connection": { + "name": "@parameters('$connections')['twitter']['connectionId']" + } + }, + "path": "/posttweet", + "queries": { + "tweetText": "@{outputs('Build_Tweet_Post')}" + } + } + } + }, + "outputs": {} + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/connections', variables('twitterConnector').connectionName)]" + ] + } + ] +} \ No newline at end of file diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.parameters.json b/resources/logappdeploy.eventgridhandler-twitter.azpls.parameters.json new file mode 100644 index 0000000..c4c7464 --- /dev/null +++ b/resources/logappdeploy.eventgridhandler-twitter.azpls.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "value": "" + } + } +} diff --git a/resources/logappdeploy.subscription.bicep b/resources/logappdeploy.subscription.bicep index 9c29d4c..691ee79 100644 --- a/resources/logappdeploy.subscription.bicep +++ b/resources/logappdeploy.subscription.bicep @@ -121,10 +121,10 @@ resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { inputs: { method: 'POST' uri: 'https://@{parameters(\'functionAppName\')}.azurewebsites.net/api/subscribe' - body: '@outputs(\'Build_Request_Payload\')' headers: { 'x-functions-key': '@parameters(\'functionAppKey\')' } + body: '@outputs(\'Build_Request_Payload\')' } } } diff --git a/resources/logappdeploy.subscription.json b/resources/logappdeploy.subscription.json index 30debf4..d4fc5ce 100644 --- a/resources/logappdeploy.subscription.json +++ b/resources/logappdeploy.subscription.json @@ -135,10 +135,10 @@ "inputs": { "method": "POST", "uri": "https://@{parameters('functionAppName')}.azurewebsites.net/api/subscribe", - "body": "@outputs('Build_Request_Payload')", "headers": { "x-functions-key": "@parameters('functionAppKey')" - } + }, + "body": "@outputs('Build_Request_Payload')" } } }, diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs index 0aa2129..845cafa 100644 --- a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs +++ b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs @@ -14,7 +14,7 @@ using Newtonsoft.Json; -namespace YouTubeWebSubSubscriptionHandler.FunctionApp +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp { /// /// This represents the HTTP trigger entity to fetch YouTube video details. diff --git a/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs new file mode 100644 index 0000000..6cb7104 --- /dev/null +++ b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs @@ -0,0 +1,20 @@ +using System; + +using FluentAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DevRelKr.YouTubeWebSubSubscriptionHandler.FunctionApp.Tests +{ + [TestClass] + public class VideoDetailsHttpTriggerTests + { + [TestMethod] + public void Given_Null_Parameter_When_Initiated_Then_It_Should_Throw_Exception() + { + Action action = () => new VideoDetailsHttpTrigger(null); + + action.Should().Throw(); + } + } +} From 4730fa6de79ef5e0ca226a3a50453535e2115516 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 18:16:15 +0900 Subject: [PATCH 11/21] Disable an endpoint --- resources/azuredeploy.bicep | 3 +++ resources/azuredeploy.json | 3 +++ ...deoDetailsHttpTrigger.cs => VideoDetailsHttpTrigger.cs.bak} | 0 ...HttpTriggerTests.cs => VideoDetailsHttpTriggerTests.cs.bak} | 0 4 files changed, 6 insertions(+) rename src/YouTubeWebSubSubscriptionHandler.FunctionApp/{VideoDetailsHttpTrigger.cs => VideoDetailsHttpTrigger.cs.bak} (100%) rename test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/{VideoDetailsHttpTriggerTests.cs => VideoDetailsHttpTriggerTests.cs.bak} (100%) diff --git a/resources/azuredeploy.bicep b/resources/azuredeploy.bicep index 0c06d05..0e19207 100644 --- a/resources/azuredeploy.bicep +++ b/resources/azuredeploy.bicep @@ -144,6 +144,9 @@ resource csplan 'Microsoft.Web/serverfarms@2020-06-01' = { name: 'Y1' tier: 'Dynamic' } + properties: { + reserved: true + } } var functionApp = { diff --git a/resources/azuredeploy.json b/resources/azuredeploy.json index dd69afc..9f39848 100644 --- a/resources/azuredeploy.json +++ b/resources/azuredeploy.json @@ -177,6 +177,9 @@ "sku": { "name": "Y1", "tier": "Dynamic" + }, + "properties": { + "reserved": true } }, { diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs.bak similarity index 100% rename from src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs rename to src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs.bak diff --git a/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs.bak similarity index 100% rename from test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs rename to test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs.bak From 52b3c42ceb76296a30bd31e1300cfce7dfd25d26 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 18:29:33 +0900 Subject: [PATCH 12/21] Update workflow --- .github/workflows/dev.yaml | 54 ++++++++++++++++++------------------- resources/azuredeploy.bicep | 6 ++--- resources/azuredeploy.json | 3 --- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 1c7a436..409beb1 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -115,30 +115,30 @@ jobs: appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} reset: true - - name: Update FunctionApp settings - uses: azure/CLI@v1 - with: - inlineScript: | - settings=$(az functionapp config appsettings set \ - -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ - --settings WebSub__CallbackKey=$(az functionapp function keys list \ - -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ - --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ - --query "default" \ - -o tsv)) - - - name: Run Bicep build - uses: aliencube/bicep-build-actions@v0.1 - with: - files: '**/*.bicep' - - - name: Deploy LogicApp for scheduled subscription to WebSub - uses: Azure/arm-deploy@v1.0.1 - with: - subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} - resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} - deploymentName: 'ytwebsub' - template: 'resources/logappdeploy.subscription.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} + # - name: Update FunctionApp settings + # uses: azure/CLI@v1 + # with: + # inlineScript: | + # settings=$(az functionapp config appsettings set \ + # -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + # -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + # --settings WebSub__CallbackKey=$(az functionapp function keys list \ + # -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + # -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + # --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ + # --query "default" \ + # -o tsv)) + + # - name: Run Bicep build + # uses: aliencube/bicep-build-actions@v0.1 + # with: + # files: '**/*.bicep' + + # - name: Deploy LogicApp for scheduled subscription to WebSub + # uses: Azure/arm-deploy@v1.0.1 + # with: + # subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + # resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + # deploymentName: 'ytwebsub' + # template: 'resources/logappdeploy.subscription.json' + # parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} diff --git a/resources/azuredeploy.bicep b/resources/azuredeploy.bicep index 0e19207..4a24dda 100644 --- a/resources/azuredeploy.bicep +++ b/resources/azuredeploy.bicep @@ -144,9 +144,9 @@ resource csplan 'Microsoft.Web/serverfarms@2020-06-01' = { name: 'Y1' tier: 'Dynamic' } - properties: { - reserved: true - } + // properties: { + // reserved: true + // } } var functionApp = { diff --git a/resources/azuredeploy.json b/resources/azuredeploy.json index 9f39848..dd69afc 100644 --- a/resources/azuredeploy.json +++ b/resources/azuredeploy.json @@ -177,9 +177,6 @@ "sku": { "name": "Y1", "tier": "Dynamic" - }, - "properties": { - "reserved": true } }, { From 1b126adfcba61989a6248b2f3b24b79cce040f53 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 20:16:02 +0900 Subject: [PATCH 13/21] Add missing bicep parameters --- .github/workflows/dev.yaml | 56 +++++++++---------- .github/workflows/release.yaml | 4 +- resources/azuredeploy.bicep | 16 ++++++ resources/azuredeploy.json | 19 ++++++- ...gger.cs.bak => VideoDetailsHttpTrigger.cs} | 0 ...cs.bak => VideoDetailsHttpTriggerTests.cs} | 0 6 files changed, 64 insertions(+), 31 deletions(-) rename src/YouTubeWebSubSubscriptionHandler.FunctionApp/{VideoDetailsHttpTrigger.cs.bak => VideoDetailsHttpTrigger.cs} (100%) rename test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/{VideoDetailsHttpTriggerTests.cs.bak => VideoDetailsHttpTriggerTests.cs} (100%) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 409beb1..0004d50 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -47,7 +47,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/azuredeploy.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} youtubeApiKey=${{ secrets.YOUTUBE_API_KEY_DEV }} youtubeFetchParts=${{ secrets.YOUTUBE_API_FETCH_PARTS }} functionapp_build_test_deploy_dev: name: 'DEV: FunctionApp Build, Test & Deploy' @@ -115,30 +115,30 @@ jobs: appName: ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} reset: true - # - name: Update FunctionApp settings - # uses: azure/CLI@v1 - # with: - # inlineScript: | - # settings=$(az functionapp config appsettings set \ - # -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - # -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ - # --settings WebSub__CallbackKey=$(az functionapp function keys list \ - # -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - # -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ - # --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ - # --query "default" \ - # -o tsv)) - - # - name: Run Bicep build - # uses: aliencube/bicep-build-actions@v0.1 - # with: - # files: '**/*.bicep' - - # - name: Deploy LogicApp for scheduled subscription to WebSub - # uses: Azure/arm-deploy@v1.0.1 - # with: - # subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} - # resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} - # deploymentName: 'ytwebsub' - # template: 'resources/logappdeploy.subscription.json' - # parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} + - name: Update FunctionApp settings + uses: azure/CLI@v1 + with: + inlineScript: | + settings=$(az functionapp config appsettings set \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --settings WebSub__CallbackKey=$(az functionapp function keys list \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ secrets.RESOURCE_FUNCTIONAPP_NAME_DEV }} \ + --function-name ${{ secrets.RESOURCE_FUNCTION_NAME_CALLBACK }} \ + --query "default" \ + -o tsv)) + + - name: Run Bicep build + uses: aliencube/bicep-build-actions@v0.1 + with: + files: '**/*.bicep' + + - name: Deploy LogicApp for scheduled subscription to WebSub + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.subscription.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0aef9e2..3baf2b7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -47,7 +47,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'dvrl.kr' template: 'resources/azuredeploy.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} youtubeApiKey=${{ secrets.YOUTUBE_API_KEY_DEV }} youtubeFetchParts=${{ secrets.YOUTUBE_API_FETCH_PARTS }} arm_template_build_test_deploy_prod: name: 'PROD: ARM Templates Build, Test & Deploy' @@ -89,7 +89,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} deploymentName: 'dvrl.kr' template: 'resources/azuredeploy.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_PROD }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_PROD }} youtubeApiKey=${{ secrets.YOUTUBE_API_KEY_PROD }} youtubeFetchParts=${{ secrets.YOUTUBE_API_FETCH_PARTS }} functionapp_build_test_deploy_dev: name: 'DEV: FunctionApp Build, Test & Deploy' diff --git a/resources/azuredeploy.bicep b/resources/azuredeploy.bicep index 4a24dda..bea8c0b 100644 --- a/resources/azuredeploy.bicep +++ b/resources/azuredeploy.bicep @@ -54,6 +54,12 @@ param functionAppTimezone string = 'Korea Standard Time' param websubSubscriptionUri string = 'https://pubsubhubbub.appspot.com/subscribe' param websubCallbackEndpoint string = 'api/callback' +// YouTube +param youtubeApiKey string { + secure: true +} +param youtubeFetchParts string = 'snippet' + var metadata = { longName: '{0}-${name}-${env}-${locationCode}{1}' shortName: '{0}${name}${env}${locationCode}' @@ -155,6 +161,8 @@ var functionApp = { environment: functionAppEnvironment runtime: functionAppWorkerRuntime timezone: functionAppTimezone + youtubeApiKey: youtubeApiKey + youtubeFetchParts: youtubeFetchParts } var websub = { @@ -232,6 +240,14 @@ resource fncapp 'Microsoft.Web/sites@2020-06-01' = { name: 'EventGrid__Topic__AccessKey' value: listKeys(evtgrdTopic.id, '2020-06-01').key1 } + { + name: 'YouTube__ApiKey' + value: functionApp.youtubeApiKey + } + { + name: 'YouTube__FetchParts' + value: functionApp.youtubeFetchParts + } ] } } diff --git a/resources/azuredeploy.json b/resources/azuredeploy.json index dd69afc..6a3da7f 100644 --- a/resources/azuredeploy.json +++ b/resources/azuredeploy.json @@ -68,6 +68,13 @@ "websubCallbackEndpoint": { "type": "string", "defaultValue": "api/callback" + }, + "youtubeApiKey": { + "type": "secureString" + }, + "youtubeFetchParts": { + "type": "string", + "defaultValue": "snippet" } }, "functions": [], @@ -104,7 +111,9 @@ "location": "[parameters('location')]", "environment": "[parameters('functionAppEnvironment')]", "runtime": "[parameters('functionAppWorkerRuntime')]", - "timezone": "[parameters('functionAppTimezone')]" + "timezone": "[parameters('functionAppTimezone')]", + "youtubeApiKey": "[parameters('youtubeApiKey')]", + "youtubeFetchParts": "[parameters('youtubeFetchParts')]" }, "websub": { "subscriptionUri": "[parameters('websubSubscriptionUri')]", @@ -249,6 +258,14 @@ { "name": "EventGrid__Topic__AccessKey", "value": "[listKeys(resourceId('Microsoft.EventGrid/topics', variables('eventgrid').name), '2020-06-01').key1]" + }, + { + "name": "YouTube__ApiKey", + "value": "[variables('functionApp').youtubeApiKey]" + }, + { + "name": "YouTube__FetchParts", + "value": "[variables('functionApp').youtubeFetchParts]" } ] } diff --git a/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs.bak b/src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs similarity index 100% rename from src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs.bak rename to src/YouTubeWebSubSubscriptionHandler.FunctionApp/VideoDetailsHttpTrigger.cs diff --git a/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs.bak b/test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs similarity index 100% rename from test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs.bak rename to test/YouTubeWebSubSubscriptionHandler.FunctionApp.Tests/VideoDetailsHttpTriggerTests.cs From 093175a381c4178fbe46e6f6ae0ef513a45a3a1c Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 20:46:04 +0900 Subject: [PATCH 14/21] Update Logic Apps to handle Twitter posts --- .github/workflows/dev.yaml | 9 +++++++++ .github/workflows/feature.yaml | 9 +++++++++ .github/workflows/release.yaml | 18 ++++++++++++++++++ ...deploy.eventgridhandler-twitter.azpls.bicep | 10 ---------- ...pdeploy.eventgridhandler-twitter.azpls.json | 10 +--------- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 0004d50..98bad1d 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -142,3 +142,12 @@ jobs: deploymentName: 'ytwebsub' template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} + + - name: Deploy LogicApp as EventGrid subscription handler + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 50d3751..a9fc254 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -129,3 +129,12 @@ jobs: deploymentName: 'ytwebsub' template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} + + - name: Deploy LogicApp as EventGrid subscription handler + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3baf2b7..fe33ab9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -185,6 +185,15 @@ jobs: template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} + - name: Deploy LogicApp as EventGrid subscription handler + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + functionapp_build_test_deploy_prod: name: 'PROD: FunctionApp Build, Test & Deploy' needs: @@ -279,3 +288,12 @@ jobs: deploymentName: 'ytwebsub' template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_PROD }} + + - name: Deploy LogicApp as EventGrid subscription handler + uses: Azure/arm-deploy@v1.0.1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep b/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep index 6712105..7cbc91a 100644 --- a/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep +++ b/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep @@ -24,14 +24,6 @@ param logicAppAcceptedTitleSegment string = '애저듣보잡' // Function App param functionName string = 'FetchAsync' -// Twitter -param twitterApiKey string { - secure: true -} -param twitterApiSecret string { - secure: true -} - var metadata = { longName: '{0}-${name}-${env}-${locationCode}{1}' shortName: '{0}${name}${env}${locationCode}' @@ -42,8 +34,6 @@ var twitterConnector = { connectionId: '${resourceGroup().id}/providers/Microsoft.Web/connections/${format(metadata.longName, 'apicon', '-twitter-azpls')}' connectionName: format(metadata.longName, 'apicon', '-twitter-azpls') location: location - apiKey: twitterApiKey - apiSecret: twitterApiSecret } resource apiconTwitter 'Microsoft.Web/connections@2016-06-01' = { diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.json b/resources/logappdeploy.eventgridhandler-twitter.azpls.json index 1c62295..ce9b05a 100644 --- a/resources/logappdeploy.eventgridhandler-twitter.azpls.json +++ b/resources/logappdeploy.eventgridhandler-twitter.azpls.json @@ -33,12 +33,6 @@ "functionName": { "type": "string", "defaultValue": "FetchAsync" - }, - "twitterApiKey": { - "type": "secureString" - }, - "twitterApiSecret": { - "type": "secureString" } }, "functions": [], @@ -51,9 +45,7 @@ "id": "[format('{0}/providers/Microsoft.Web/locations/{1}/managedApis/twitter', subscription().id, parameters('location'))]", "connectionId": "[format('{0}/providers/Microsoft.Web/connections/{1}', resourceGroup().id, format(variables('metadata').longName, 'apicon', '-twitter-azpls'))]", "connectionName": "[format(variables('metadata').longName, 'apicon', '-twitter-azpls')]", - "location": "[parameters('location')]", - "apiKey": "[parameters('twitterApiKey')]", - "apiSecret": "[parameters('twitterApiSecret')]" + "location": "[parameters('location')]" }, "logicApp": { "name": "[format(variables('metadata').longName, 'logapp', '-eventgrid-sub-handler-twitter')]", From 6a7b0132cfa4c189863f5017dee9ef52b0f42d30 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 22:21:27 +0900 Subject: [PATCH 15/21] Add EventGrid Subscription provisioning --- .github/workflows/dev.yaml | 35 +++++++++- .github/workflows/feature.yaml | 42 ++++++++++- .github/workflows/release.yaml | 70 +++++++++++++++++-- resources/azuredeploy.bicep | 2 + resources/azuredeploy.json | 8 ++- ...eploy.eventgridhandler-twitter.azpls.bicep | 2 + ...deploy.eventgridhandler-twitter.azpls.json | 8 ++- resources/logappdeploy.subscription.bicep | 2 + resources/logappdeploy.subscription.json | 8 ++- 9 files changed, 163 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 98bad1d..e3cf4bf 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -7,6 +7,7 @@ on: env: FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' + EVENTGRID_NAME_DEV: '' jobs: arm_template_build_test_deploy_dev: @@ -41,7 +42,8 @@ jobs: echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' - name: Deploy ARM templates - uses: Azure/arm-deploy@v1.0.1 + id: arm + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} @@ -49,6 +51,11 @@ jobs: template: 'resources/azuredeploy.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} youtubeApiKey=${{ secrets.YOUTUBE_API_KEY_DEV }} youtubeFetchParts=${{ secrets.YOUTUBE_API_FETCH_PARTS }} + - name: Set EventGrid name + shell: bash + run: | + echo "EVENTGRID_NAME_DEV=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + functionapp_build_test_deploy_dev: name: 'DEV: FunctionApp Build, Test & Deploy' needs: @@ -135,7 +142,8 @@ jobs: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - uses: Azure/arm-deploy@v1.0.1 + id: logappWebSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} @@ -144,10 +152,31 @@ jobs: parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} - name: Deploy LogicApp as EventGrid subscription handler - uses: Azure/arm-deploy@v1.0.1 + id: logappEventGridSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + + - name: Provision EventGrid subscription + uses: azure/CLI@v1 + with: + inlineScript: | + sub=az eventgrid event-subscription create \ + -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ env.EVENTGRID_NAME_DEV }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index a9fc254..5a63be4 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -8,6 +8,7 @@ on: env: FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' + EVENTGRID_NAME_DEV: '' jobs: arm_template_build_test: @@ -36,6 +37,21 @@ jobs: run: | echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' + - name: Deploy ARM templates + id: arm + uses: Azure/arm-deploy@v1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/azuredeploy.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} youtubeApiKey=${{ secrets.YOUTUBE_API_KEY_DEV }} youtubeFetchParts=${{ secrets.YOUTUBE_API_FETCH_PARTS }} + + - name: Set EventGrid name + shell: bash + run: | + echo "EVENTGRID_NAME_DEV=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + functionapp_build_test_deploy_dev: name: 'WIP: FunctionApp Build & Test' needs: @@ -122,7 +138,8 @@ jobs: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - uses: Azure/arm-deploy@v1.0.1 + id: logappWebSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} @@ -131,10 +148,31 @@ jobs: parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} - name: Deploy LogicApp as EventGrid subscription handler - uses: Azure/arm-deploy@v1.0.1 + id: logappEventGridSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + + - name: Provision EventGrid subscription + uses: azure/CLI@v1 + with: + inlineScript: | + sub=az eventgrid event-subscription create \ + -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ env.EVENTGRID_NAME_DEV }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fe33ab9..00f289e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,6 +7,8 @@ on: env: FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' + EVENTGRID_NAME_DEV: '' + EVENTGRID_NAME_PROD: '' jobs: arm_template_build_test_deploy_dev: @@ -41,7 +43,8 @@ jobs: echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' - name: Deploy ARM templates - uses: Azure/arm-deploy@v1.0.1 + id: arm + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} @@ -49,6 +52,11 @@ jobs: template: 'resources/azuredeploy.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_DEV }} youtubeApiKey=${{ secrets.YOUTUBE_API_KEY_DEV }} youtubeFetchParts=${{ secrets.YOUTUBE_API_FETCH_PARTS }} + - name: Set EventGrid name + shell: bash + run: | + echo "EVENTGRID_NAME_DEV=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + arm_template_build_test_deploy_prod: name: 'PROD: ARM Templates Build, Test & Deploy' needs: @@ -83,7 +91,8 @@ jobs: echo 'Results: ${{ toJSON(fromJSON(steps.armtest.outputs.results)) }}' - name: Deploy ARM templates - uses: Azure/arm-deploy@v1.0.1 + id: arm + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} @@ -91,6 +100,11 @@ jobs: template: 'resources/azuredeploy.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} functionAppEnvironment=${{ secrets.RESOURCE_FUNCTIONAPP_ENVIRONMENT_PROD }} youtubeApiKey=${{ secrets.YOUTUBE_API_KEY_PROD }} youtubeFetchParts=${{ secrets.YOUTUBE_API_FETCH_PARTS }} + - name: Set EventGrid name + shell: bash + run: | + echo "EVENTGRID_NAME_PROD=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + functionapp_build_test_deploy_dev: name: 'DEV: FunctionApp Build, Test & Deploy' needs: @@ -177,7 +191,8 @@ jobs: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - uses: Azure/arm-deploy@v1.0.1 + id: logappWebSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} @@ -186,7 +201,8 @@ jobs: parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} - name: Deploy LogicApp as EventGrid subscription handler - uses: Azure/arm-deploy@v1.0.1 + id: logappEventGridSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} @@ -194,6 +210,26 @@ jobs: template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + - name: Provision EventGrid subscription + uses: azure/CLI@v1 + with: + inlineScript: | + sub=az eventgrid event-subscription create \ + -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ env.EVENTGRID_NAME_DEV }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) + functionapp_build_test_deploy_prod: name: 'PROD: FunctionApp Build, Test & Deploy' needs: @@ -281,7 +317,8 @@ jobs: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - uses: Azure/arm-deploy@v1.0.1 + id: logappWebSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} @@ -290,10 +327,31 @@ jobs: parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_PROD }} - name: Deploy LogicApp as EventGrid subscription handler - uses: Azure/arm-deploy@v1.0.1 + id: logappEventGridSub + uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + + - name: Provision EventGrid subscription + uses: azure/CLI@v1 + with: + inlineScript: | + sub=az eventgrid event-subscription create \ + -n ${{ env.EVENTGRID_NAME_PROD }}-sub \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ + -n ${{ env.EVENTGRID_NAME_PROD }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ + -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) diff --git a/resources/azuredeploy.bicep b/resources/azuredeploy.bicep index bea8c0b..85b99eb 100644 --- a/resources/azuredeploy.bicep +++ b/resources/azuredeploy.bicep @@ -252,3 +252,5 @@ resource fncapp 'Microsoft.Web/sites@2020-06-01' = { } } } + +output eventgridName string = format(metadata.longName, 'evtgrd', '') diff --git a/resources/azuredeploy.json b/resources/azuredeploy.json index 6a3da7f..823186f 100644 --- a/resources/azuredeploy.json +++ b/resources/azuredeploy.json @@ -277,5 +277,11 @@ "[resourceId('Microsoft.Storage/storageAccounts', variables('storage').name)]" ] } - ] + ], + "outputs": { + "eventgridName": { + "type": "string", + "value": "[format(variables('metadata').longName, 'evtgrd', '')]" + } + } } \ No newline at end of file diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep b/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep index 7cbc91a..d18ee4f 100644 --- a/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep +++ b/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep @@ -263,3 +263,5 @@ resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { } } } + +output logicAppName string = logapp.name diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.json b/resources/logappdeploy.eventgridhandler-twitter.azpls.json index ce9b05a..1b7aa74 100644 --- a/resources/logappdeploy.eventgridhandler-twitter.azpls.json +++ b/resources/logappdeploy.eventgridhandler-twitter.azpls.json @@ -280,5 +280,11 @@ "[resourceId('Microsoft.Web/connections', variables('twitterConnector').connectionName)]" ] } - ] + ], + "outputs": { + "logicAppName": { + "type": "string", + "value": "[variables('logicApp').name]" + } + } } \ No newline at end of file diff --git a/resources/logappdeploy.subscription.bicep b/resources/logappdeploy.subscription.bicep index 691ee79..2e83313 100644 --- a/resources/logappdeploy.subscription.bicep +++ b/resources/logappdeploy.subscription.bicep @@ -132,3 +132,5 @@ resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { } } } + +output logicAppName string = logapp.name diff --git a/resources/logappdeploy.subscription.json b/resources/logappdeploy.subscription.json index d4fc5ce..6ffa0d6 100644 --- a/resources/logappdeploy.subscription.json +++ b/resources/logappdeploy.subscription.json @@ -146,5 +146,11 @@ } } } - ] + ], + "outputs": { + "logicAppName": { + "type": "string", + "value": "[variables('logicApp').name]" + } + } } \ No newline at end of file From 46036feb93facd3c1361d215999917a81874207c Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 22:53:53 +0900 Subject: [PATCH 16/21] Update workflows --- .github/workflows/dev.yaml | 1 + .github/workflows/feature.yaml | 1 + .github/workflows/release.yaml | 2 ++ 3 files changed, 4 insertions(+) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index e3cf4bf..332bcdc 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -165,6 +165,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ --source-resource-id $(az eventgrid topic show \ diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 5a63be4..a65c663 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -161,6 +161,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ --source-resource-id $(az eventgrid topic show \ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 00f289e..9dcaa03 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -214,6 +214,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ --source-resource-id $(az eventgrid topic show \ @@ -340,6 +341,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_PROD }}-sub \ --source-resource-id $(az eventgrid topic show \ From 67b17c5869029c40b2055467214975a2e58beb07 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 2 Jan 2021 23:27:45 +0900 Subject: [PATCH 17/21] Update workflow --- .github/workflows/dev.yaml | 1 + .github/workflows/feature.yaml | 1 + .github/workflows/release.yaml | 2 ++ 3 files changed, 4 insertions(+) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 332bcdc..b74e978 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -165,6 +165,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index a65c663..89b9c77 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -161,6 +161,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9dcaa03..8fcd2e0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -214,6 +214,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ @@ -341,6 +342,7 @@ jobs: uses: azure/CLI@v1 with: inlineScript: | + az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ -n ${{ env.EVENTGRID_NAME_PROD }}-sub \ From b95e0099c62e8e7a8ee651cae40f7719bce5f092 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 3 Jan 2021 00:08:09 +0900 Subject: [PATCH 18/21] Update workflows --- .github/workflows/dev.yaml | 25 ++++++++++++++--- .github/workflows/feature.yaml | 25 ++++++++++++++--- .github/workflows/release.yaml | 50 ++++++++++++++++++++++++++++------ 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index b74e978..cc2bffb 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -7,7 +7,6 @@ on: env: FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' - EVENTGRID_NAME_DEV: '' jobs: arm_template_build_test_deploy_dev: @@ -54,7 +53,13 @@ jobs: - name: Set EventGrid name shell: bash run: | - echo "EVENTGRID_NAME_DEV=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + echo ${{ steps.arm.outputs.eventgridName }} > eventgrid_name_dev.txt + + - name: Upload EventGrid name + uses: actions/upload-artifact@v2 + with: + name: eventgrid_name_dev + path: eventgrid_name_dev.txt functionapp_build_test_deploy_dev: name: 'DEV: FunctionApp Build, Test & Deploy' @@ -161,6 +166,18 @@ jobs: template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + - name: Download EventGrid name + uses: actions/download-artifact@v2 + with: + name: eventgrid_name_dev + + - name: Get EventGrid name + id: eventgrid + shell: bash + run: | + name=$(cat eventgrid_name_dev.txt) + echo "::set-output name=name::$name" + - name: Provision EventGrid subscription uses: azure/CLI@v1 with: @@ -168,10 +185,10 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ - -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ env.EVENTGRID_NAME_DEV }}-topic \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ --query "id" -o tsv) \ --event-delivery-schema cloudeventschemav1_0 \ --endpoint-type webhook \ diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 89b9c77..195a7f7 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -8,7 +8,6 @@ on: env: FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' - EVENTGRID_NAME_DEV: '' jobs: arm_template_build_test: @@ -50,7 +49,13 @@ jobs: - name: Set EventGrid name shell: bash run: | - echo "EVENTGRID_NAME_DEV=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + echo ${{ steps.arm.outputs.eventgridName }} > eventgrid_name_dev.txt + + - name: Upload EventGrid name + uses: actions/upload-artifact@v2 + with: + name: eventgrid_name_dev + path: eventgrid_name_dev.txt functionapp_build_test_deploy_dev: name: 'WIP: FunctionApp Build & Test' @@ -157,6 +162,18 @@ jobs: template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + - name: Download EventGrid name + uses: actions/download-artifact@v2 + with: + name: eventgrid_name_dev + + - name: Get EventGrid name + id: eventgrid + shell: bash + run: | + name=$(cat eventgrid_name_dev.txt) + echo "::set-output name=name::$name" + - name: Provision EventGrid subscription uses: azure/CLI@v1 with: @@ -164,10 +181,10 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ - -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ env.EVENTGRID_NAME_DEV }}-topic \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ --query "id" -o tsv) \ --event-delivery-schema cloudeventschemav1_0 \ --endpoint-type webhook \ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8fcd2e0..9061a71 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,8 +7,6 @@ on: env: FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' - EVENTGRID_NAME_DEV: '' - EVENTGRID_NAME_PROD: '' jobs: arm_template_build_test_deploy_dev: @@ -55,7 +53,13 @@ jobs: - name: Set EventGrid name shell: bash run: | - echo "EVENTGRID_NAME_DEV=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + echo ${{ steps.arm.outputs.eventgridName }} > eventgrid_name_dev.txt + + - name: Upload EventGrid name + uses: actions/upload-artifact@v2 + with: + name: eventgrid_name_dev + path: eventgrid_name_dev.txt arm_template_build_test_deploy_prod: name: 'PROD: ARM Templates Build, Test & Deploy' @@ -103,7 +107,13 @@ jobs: - name: Set EventGrid name shell: bash run: | - echo "EVENTGRID_NAME_PROD=${{ steps.arm.outputs.eventgridName }}" >> $GITHUB_ENV + echo ${{ steps.arm.outputs.eventgridName }} > eventgrid_name_prod.txt + + - name: Upload EventGrid name + uses: actions/upload-artifact@v2 + with: + name: eventgrid_name_prod + path: eventgrid_name_prod.txt functionapp_build_test_deploy_dev: name: 'DEV: FunctionApp Build, Test & Deploy' @@ -210,6 +220,18 @@ jobs: template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + - name: Download EventGrid name + uses: actions/download-artifact@v2 + with: + name: eventgrid_name_dev + + - name: Get EventGrid name + id: eventgrid + shell: bash + run: | + name=$(cat eventgrid_name_dev.txt) + echo "::set-output name=name::$name" + - name: Provision EventGrid subscription uses: azure/CLI@v1 with: @@ -217,10 +239,10 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ - -n ${{ env.EVENTGRID_NAME_DEV }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ env.EVENTGRID_NAME_DEV }}-topic \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ --query "id" -o tsv) \ --event-delivery-schema cloudeventschemav1_0 \ --endpoint-type webhook \ @@ -338,6 +360,18 @@ jobs: template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + - name: Download EventGrid name + uses: actions/download-artifact@v2 + with: + name: eventgrid_name_prod + + - name: Get EventGrid name + id: eventgrid + shell: bash + run: | + name=$(cat eventgrid_name_prod.txt) + echo "::set-output name=name::$name" + - name: Provision EventGrid subscription uses: azure/CLI@v1 with: @@ -345,10 +379,10 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=az eventgrid event-subscription create \ - -n ${{ env.EVENTGRID_NAME_PROD }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ - -n ${{ env.EVENTGRID_NAME_PROD }}-topic \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ --query "id" -o tsv) \ --event-delivery-schema cloudeventschemav1_0 \ --endpoint-type webhook \ From 45905cf209bf14e2c8f4a875473c7b7cc2ec237c Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 3 Jan 2021 00:27:48 +0900 Subject: [PATCH 19/21] Update workflows --- .github/workflows/dev.yaml | 4 ++-- .github/workflows/feature.yaml | 4 ++-- .github/workflows/release.yaml | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index cc2bffb..e6a3c5f 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -184,7 +184,7 @@ jobs: inlineScript: | az extension add -n eventgrid az extension add -n logic - sub=az eventgrid event-subscription create \ + sub=$(az eventgrid event-subscription create \ -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ @@ -198,4 +198,4 @@ jobs: -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv) + --query "value" -o tsv)) diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 195a7f7..9d691e6 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -180,7 +180,7 @@ jobs: inlineScript: | az extension add -n eventgrid az extension add -n logic - sub=az eventgrid event-subscription create \ + sub=$(az eventgrid event-subscription create \ -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ @@ -194,4 +194,4 @@ jobs: -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv) + --query "value" -o tsv)) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9061a71..7afa0d4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -238,7 +238,7 @@ jobs: inlineScript: | az extension add -n eventgrid az extension add -n logic - sub=az eventgrid event-subscription create \ + sub=$(az eventgrid event-subscription create \ -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ @@ -252,7 +252,7 @@ jobs: -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv) + --query "value" -o tsv)) functionapp_build_test_deploy_prod: name: 'PROD: FunctionApp Build, Test & Deploy' @@ -378,7 +378,7 @@ jobs: inlineScript: | az extension add -n eventgrid az extension add -n logic - sub=az eventgrid event-subscription create \ + sub=$(az eventgrid event-subscription create \ -n ${{ steps.eventgrid.outputs.name }}-sub \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ @@ -392,4 +392,4 @@ jobs: -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv) + --query "value" -o tsv)) From 97382728cb0e0df5fd9f90d74d5ef1c6dab16488 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 3 Jan 2021 20:17:59 +0900 Subject: [PATCH 20/21] Logic App for Retweets (#5) --- .github/workflows/dev.yaml | 70 +++++- .github/workflows/feature.yaml | 83 +++++-- .github/workflows/release.yaml | 141 ++++++++++-- .gitignore | 1 + ...appdeploy.eventgridhandler-retweeter.bicep | 188 ++++++++++++++++ ...gappdeploy.eventgridhandler-retweeter.json | 211 ++++++++++++++++++ ...ventgridhandler-retweeter.parameters.json} | 6 + ...gappdeploy.eventgridhandler-twitter.bicep} | 106 +++++++-- ...ogappdeploy.eventgridhandler-twitter.json} | 111 +++++++-- ...y.eventgridhandler-twitter.parameters.json | 15 ++ 10 files changed, 861 insertions(+), 71 deletions(-) create mode 100644 resources/logappdeploy.eventgridhandler-retweeter.bicep create mode 100644 resources/logappdeploy.eventgridhandler-retweeter.json rename resources/{logappdeploy.eventgridhandler-twitter.azpls.parameters.json => logappdeploy.eventgridhandler-retweeter.parameters.json} (58%) rename resources/{logappdeploy.eventgridhandler-twitter.azpls.bicep => logappdeploy.eventgridhandler-twitter.bicep} (70%) rename resources/{logappdeploy.eventgridhandler-twitter.azpls.json => logappdeploy.eventgridhandler-twitter.json} (69%) create mode 100644 resources/logappdeploy.eventgridhandler-twitter.parameters.json diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index e6a3c5f..2454a86 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -141,13 +141,29 @@ jobs: --query "default" \ -o tsv)) + logicapp_build_deploy_dev: + name: 'DEV: LogicApp Build & Deploy' + needs: + - functionapp_build_test_deploy_dev + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 with: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - id: logappWebSub + id: websub uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} @@ -156,15 +172,25 @@ jobs: template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} - - name: Deploy LogicApp as EventGrid subscription handler - id: logappEventGridSub + - name: Deploy LogicApp as EventGrid subscription handler for Twitter + id: twitter + uses: Azure/arm-deploy@v1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} youTubeAcceptedTitleSegment=${{ secrets.YOUTUBE_ACCEPTED_TITLE_SEGMENT }} youTubeAcceptedEventType=${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} + + - name: Deploy LogicApp as EventGrid subscription handler for ReTweet + id: retweeter uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' - template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + template: 'resources/logappdeploy.eventgridhandler-retweeter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -178,14 +204,38 @@ jobs: name=$(cat eventgrid_name_dev.txt) echo "::set-output name=name::$name" - - name: Provision EventGrid subscription + - name: Provision EventGrid subscription for Twitter + uses: azure/CLI@v1 + with: + inlineScript: | + az extension add -n eventgrid + az extension add -n logic + sub=$(az eventgrid event-subscription create \ + -n ${{ steps.eventgrid.outputs.name }}-sub-twitter-${{ secrets.TWITTER_PROFILE_ID }} \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.twitter.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }}) + + - name: Provision EventGrid subscription for ReTweet uses: azure/CLI@v1 with: inlineScript: | az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \ @@ -196,6 +246,8 @@ jobs: -m POST \ -u "https://management.azure.com$(az logic workflow show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + -n ${{ steps.retweeter.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv)) + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }}) diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 9d691e6..68698c9 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -10,8 +10,8 @@ env: FUNCTIONAPP_PATH: 'src/YouTubeWebSubSubscriptionHandler.FunctionApp' jobs: - arm_template_build_test: - name: 'WIP: ARM Templates Build & Test' + arm_template_build_test_deploy_dev: + name: 'WIP: ARM Templates Build, Test & Deploy' runs-on: ubuntu-latest @@ -19,6 +19,11 @@ jobs: - name: Checkout the repo uses: actions/checkout@v2 + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 with: @@ -58,9 +63,9 @@ jobs: path: eventgrid_name_dev.txt functionapp_build_test_deploy_dev: - name: 'WIP: FunctionApp Build & Test' + name: 'WIP: FunctionApp Build, Test & Deploy' needs: - - arm_template_build_test + - arm_template_build_test_deploy_dev runs-on: ubuntu-latest @@ -137,13 +142,29 @@ jobs: --query "default" \ -o tsv)) + logicapp_build_deploy_dev: + name: 'WIP: LogicApp Build & Deploy' + needs: + - functionapp_build_test_deploy_dev + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 with: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - id: logappWebSub + id: websub uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} @@ -152,15 +173,25 @@ jobs: template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} - - name: Deploy LogicApp as EventGrid subscription handler - id: logappEventGridSub + - name: Deploy LogicApp as EventGrid subscription handler for Twitter + id: twitter + uses: Azure/arm-deploy@v1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} youTubeAcceptedTitleSegment=${{ secrets.YOUTUBE_ACCEPTED_TITLE_SEGMENT }} youTubeAcceptedEventType=${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} + + - name: Deploy LogicApp as EventGrid subscription handler for ReTweet + id: retweeter uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' - template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + template: 'resources/logappdeploy.eventgridhandler-retweeter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -174,14 +205,38 @@ jobs: name=$(cat eventgrid_name_dev.txt) echo "::set-output name=name::$name" - - name: Provision EventGrid subscription + - name: Provision EventGrid subscription for Twitter + uses: azure/CLI@v1 + with: + inlineScript: | + az extension add -n eventgrid + az extension add -n logic + sub=$(az eventgrid event-subscription create \ + -n ${{ steps.eventgrid.outputs.name }}-sub-twitter-${{ secrets.TWITTER_PROFILE_ID }} \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.twitter.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }}) + + - name: Provision EventGrid subscription for ReTweet uses: azure/CLI@v1 with: inlineScript: | az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \ @@ -192,6 +247,8 @@ jobs: -m POST \ -u "https://management.azure.com$(az logic workflow show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + -n ${{ steps.retweeter.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv)) + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }}) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7afa0d4..d26cc60 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -195,13 +195,29 @@ jobs: --query "default" \ -o tsv)) + logicapp_build_deploy_dev: + name: 'DEV: LogicApp Build & Deploy' + needs: + - functionapp_build_test_deploy_dev + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_DEV }} + - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 with: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - id: logappWebSub + id: websub uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} @@ -210,15 +226,25 @@ jobs: template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_DEV }} - - name: Deploy LogicApp as EventGrid subscription handler - id: logappEventGridSub + - name: Deploy LogicApp as EventGrid subscription handler for Twitter + id: twitter + uses: Azure/arm-deploy@v1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} youTubeAcceptedTitleSegment=${{ secrets.YOUTUBE_ACCEPTED_TITLE_SEGMENT }} youTubeAcceptedEventType=${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} + + - name: Deploy LogicApp as EventGrid subscription handler for ReTweet + id: retweeter uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_DEV }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' - template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + template: 'resources/logappdeploy.eventgridhandler-retweeter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -232,14 +258,38 @@ jobs: name=$(cat eventgrid_name_dev.txt) echo "::set-output name=name::$name" - - name: Provision EventGrid subscription + - name: Provision EventGrid subscription for Twitter + uses: azure/CLI@v1 + with: + inlineScript: | + az extension add -n eventgrid + az extension add -n logic + sub=$(az eventgrid event-subscription create \ + -n ${{ steps.eventgrid.outputs.name }}-sub-twitter-${{ secrets.TWITTER_PROFILE_ID }} \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ + -n ${{ steps.twitter.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }}) + + - name: Provision EventGrid subscription for ReTweet uses: azure/CLI@v1 with: inlineScript: | az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \ @@ -250,9 +300,11 @@ jobs: -m POST \ -u "https://management.azure.com$(az logic workflow show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ - -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + -n ${{ steps.retweeter.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv)) + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }}) functionapp_build_test_deploy_prod: name: 'PROD: FunctionApp Build, Test & Deploy' @@ -335,13 +387,30 @@ jobs: --query "default" \ -o tsv)) + logicapp_build_deploy_prod: + name: 'PROD: LogicApp Build & Deploy' + needs: + - functionapp_build_test_deploy_dev + - functionapp_build_test_deploy_prod + + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + + - name: Login to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS_PROD }} + - name: Run Bicep build uses: aliencube/bicep-build-actions@v0.1 with: files: '**/*.bicep' - name: Deploy LogicApp for scheduled subscription to WebSub - id: logappWebSub + id: websub uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} @@ -350,15 +419,25 @@ jobs: template: 'resources/logappdeploy.subscription.json' parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} logicAppSubscriptionTopicUri=${{ secrets.YOUTUBE_TOPIC_URL_PROD }} - - name: Deploy LogicApp as EventGrid subscription handler - id: logappEventGridSub + - name: Deploy LogicApp as EventGrid subscription handler for Twitter + id: twitter + uses: Azure/arm-deploy@v1 + with: + subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} + resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} + deploymentName: 'ytwebsub' + template: 'resources/logappdeploy.eventgridhandler-twitter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} youTubeAcceptedTitleSegment=${{ secrets.YOUTUBE_ACCEPTED_TITLE_SEGMENT }} youTubeAcceptedEventType=${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} + + - name: Deploy LogicApp as EventGrid subscription handler for ReTweet + id: retweeter uses: Azure/arm-deploy@v1 with: subscriptionId: ${{ secrets.SUBSCRIPTION_ID_PROD }} resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} deploymentName: 'ytwebsub' - template: 'resources/logappdeploy.eventgridhandler-twitter.azpls.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} + template: 'resources/logappdeploy.eventgridhandler-retweeter.json' + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -372,14 +451,38 @@ jobs: name=$(cat eventgrid_name_prod.txt) echo "::set-output name=name::$name" - - name: Provision EventGrid subscription + - name: Provision EventGrid subscription for Twitter + uses: azure/CLI@v1 + with: + inlineScript: | + az extension add -n eventgrid + az extension add -n logic + sub=$(az eventgrid event-subscription create \ + -n ${{ steps.eventgrid.outputs.name }}-sub-twitter-${{ secrets.TWITTER_PROFILE_ID }} \ + --source-resource-id $(az eventgrid topic show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ + -n ${{ steps.eventgrid.outputs.name }}-topic \ + --query "id" -o tsv) \ + --event-delivery-schema cloudeventschemav1_0 \ + --endpoint-type webhook \ + --endpoint $(az rest \ + -m POST \ + -u "https://management.azure.com$(az logic workflow show \ + -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ + -n ${{ steps.twitter.outputs.logicAppName }} \ + --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_YOUTUBE }}) + + - name: Provision EventGrid subscription for ReTweet uses: azure/CLI@v1 with: inlineScript: | az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \ @@ -390,6 +493,8 @@ jobs: -m POST \ -u "https://management.azure.com$(az logic workflow show \ -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ - -n ${{ steps.logappEventGridSub.outputs.logicAppName }} \ + -n ${{ steps.retweeter.outputs.logicAppName }} \ --query "id" -o tsv)/triggers/manual/listCallbackUrl?api-version=2016-06-01" \ - --query "value" -o tsv)) + --query "value" -o tsv) \ + --advanced-filter type StringBeginsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} \ + --advanced-filter type StringEndsWith ${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }}) diff --git a/.gitignore b/.gitignore index 855d03a..b442451 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,4 @@ azurite/ .DS_Store *.local.json +sample.txt diff --git a/resources/logappdeploy.eventgridhandler-retweeter.bicep b/resources/logappdeploy.eventgridhandler-retweeter.bicep new file mode 100644 index 0000000..641c14a --- /dev/null +++ b/resources/logappdeploy.eventgridhandler-retweeter.bicep @@ -0,0 +1,188 @@ +// Resource name +param name string + +// Provisioning environment +param env string { + allowed: [ + 'dev' + 'test' + 'prod' + ] + default: 'dev' +} + +// Resource location +param location string = resourceGroup().location + +// Resource location code +param locationCode string = 'krc' + +// Twitter +param twitterEventType string = 'com.twitter.tweet.posted' +param twitterProfileId string +param retweeterProfileId string + +var metadata = { + longName: '{0}-${name}-${env}-${locationCode}{1}' + shortName: '{0}${name}${env}${locationCode}' +} + +var twitterConnector = { + id: '${subscription().id}/providers/Microsoft.Web/locations/${location}/managedApis/twitter' + connectionId: '${resourceGroup().id}/providers/Microsoft.Web/connections/${format(format(metadata.longName, 'apicon', '-twitter-{0}'), retweeterProfileId)}' + connectionName: format(format(metadata.longName, 'apicon', '-twitter-{0}'), retweeterProfileId) + location: location +} + +resource apiconTwitter 'Microsoft.Web/connections@2016-06-01' = { + name: twitterConnector.connectionName + location: twitterConnector.location + kind: 'V1' + properties: { + displayName: twitterConnector.connectionName + api: { + id: twitterConnector.id + } + } +} + +var logicApp = { + name: format(format(metadata.longName, 'logapp', '-eventgrid-sub-handler-retweeter-{0}'), retweeterProfileId) + location: location +} + +var twitter = { + source: 'https://twitter.com/${twitterProfileId}' + type: twitterEventType +} + +resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { + name: logicApp.name + location: logicApp.location + properties: { + state: 'Enabled' + parameters: { + '$connections': { + value: { + twitter: { + id: twitterConnector.id + connectionId: twitterConnector.connectionId + connectionName: apiconTwitter.name + } + } + } + } + definition: { + '$schema': 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#' + contentVersion: '1.0.0.0' + parameters: { + '$connections': { + type: 'object' + defaultValue: {} + } + acceptedTwitterSource: { + type: 'string' + defaultValue: twitter.source + } + acceptedTwitterType: { + type: 'string' + defaultValue: twitter.type + } + } + triggers: { + manual: { + type: 'Request' + kind: 'Http' + inputs: { + schema: { + type: 'object' + properties: { + id: { + type: 'string' + } + specversion: { + type: 'string' + } + source: { + type: 'string' + } + type: { + type: 'string' + } + time: { + type: 'string' + } + datacontenttype: { + type: 'string' + } + data: { + type: 'object' + properties: { + TweetId: { + type: 'string' + } + } + } + } + } + } + } + } + actions: { + Proceed_Only_If_Accepted_Source: { + type: 'If' + runAfter: {} + expression: { + and: [ + { + equals: [ + '@triggerBody()?[\'source\']' + '@parameters(\'acceptedTwitterSource\')' + ] + } + { + equals: [ + '@triggerBody()?[\'type\']' + '@parameters(\'acceptedTwitterType\')' + ] + } + ] + } + actions: { + Retweet_Post: { + type: 'ApiConnection' + runAfter: {} + inputs: { + method: 'POST' + host: { + connection: { + name: '@parameters(\'$connections\')[\'twitter\'][\'connectionId\']' + } + } + path: '/retweet' + queries: { + tweetId: '@triggerBody()?[\'data\']?[\'TweetId\']' + trimUser: false + } + } + } + } + else: { + actions: { + Cancel_Process_Retweeting: { + type: 'Terminate' + runAfter: {} + inputs: { + runStatus: 'Cancelled' + } + } + } + } + } + } + outputs: {} + } + } +} + +output logicAppName string = logapp.name diff --git a/resources/logappdeploy.eventgridhandler-retweeter.json b/resources/logappdeploy.eventgridhandler-retweeter.json new file mode 100644 index 0000000..41d9fd1 --- /dev/null +++ b/resources/logappdeploy.eventgridhandler-retweeter.json @@ -0,0 +1,211 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string" + }, + "env": { + "type": "string", + "defaultValue": "dev", + "allowedValues": [ + "dev", + "test", + "prod" + ] + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "locationCode": { + "type": "string", + "defaultValue": "krc" + }, + "twitterProfileId": { + "type": "string" + }, + "twitterEventType": { + "type": "string", + "defaultValue": "com.twitter.tweet.posted" + }, + "retweeterProfileId": { + "type": "string" + } + }, + "functions": [], + "variables": { + "metadata": { + "longName": "[format('{{0}}-{0}-{1}-{2}{{1}}', parameters('name'), parameters('env'), parameters('locationCode'))]", + "shortName": "[format('{{0}}{0}{1}{2}', parameters('name'), parameters('env'), parameters('locationCode'))]" + }, + "twitterConnector": { + "id": "[format('{0}/providers/Microsoft.Web/locations/{1}/managedApis/twitter', subscription().id, parameters('location'))]", + "connectionId": "[format('{0}/providers/Microsoft.Web/connections/{1}', resourceGroup().id, format(format(variables('metadata').longName, 'apicon', '-twitter-{0}'), parameters('retweeterProfileId')))]", + "connectionName": "[format(format(variables('metadata').longName, 'apicon', '-twitter-{0}'), parameters('retweeterProfileId'))]", + "location": "[parameters('location')]" + }, + "logicApp": { + "name": "[format(format(variables('metadata').longName, 'logapp', '-eventgrid-sub-handler-retweeter-{0}'), parameters('retweeterProfileId'))]", + "location": "[parameters('location')]" + }, + "twitter": { + "source": "[format('https://twitter.com/{0}', parameters('twitterProfileId'))]", + "type": "[parameters('twitterEventType')]" + } + }, + "resources": [ + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('twitterConnector').connectionName]", + "location": "[variables('twitterConnector').location]", + "kind": "V1", + "properties": { + "displayName": "[variables('twitterConnector').connectionName]", + "api": { + "id": "[variables('twitterConnector').id]" + } + } + }, + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2019-05-01", + "name": "[variables('logicApp').name]", + "location": "[variables('logicApp').location]", + "properties": { + "state": "Enabled", + "parameters": { + "$connections": { + "value": { + "twitter": { + "id": "[variables('twitterConnector').id]", + "connectionId": "[variables('twitterConnector').connectionId]", + "connectionName": "[variables('twitterConnector').connectionName]" + } + } + } + }, + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { + "type": "object", + "defaultValue": {} + }, + "acceptedTwitterSource": { + "type": "string", + "defaultValue": "[variables('twitter').source]" + }, + "acceptedTwitterType": { + "type": "string", + "defaultValue": "[variables('twitter').type]" + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "specversion": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "time": { + "type": "string" + }, + "datacontenttype": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "TweetId": { + "type": "string" + } + } + } + } + } + } + } + }, + "actions": { + "Proceed_Only_If_Accepted_Source": { + "type": "If", + "runAfter": {}, + "expression": { + "and": [ + { + "equals": [ + "@triggerBody()?['source']", + "@parameters('acceptedTwitterSource')" + ] + }, + { + "equals": [ + "@triggerBody()?['type']", + "@parameters('acceptedTwitterType')" + ] + } + ] + }, + "actions": { + "Retweet_Post": { + "type": "ApiConnection", + "runAfter": {}, + "inputs": { + "method": "POST", + "host": { + "connection": { + "name": "@parameters('$connections')['twitter']['connectionId']" + } + }, + "path": "/retweet", + "queries": { + "tweetId": "@triggerBody()?['data']?['TweetId']", + "trimUser": false + } + } + } + }, + "else": { + "actions": { + "Cancel_Process_Retweeting": { + "type": "Terminate", + "runAfter": {}, + "inputs": { + "runStatus": "Cancelled" + } + } + } + } + } + }, + "outputs": {} + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/connections', variables('twitterConnector').connectionName)]" + ] + } + ], + "outputs": { + "logicAppName": { + "type": "string", + "value": "[variables('logicApp').name]" + } + } +} \ No newline at end of file diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.parameters.json b/resources/logappdeploy.eventgridhandler-retweeter.parameters.json similarity index 58% rename from resources/logappdeploy.eventgridhandler-twitter.azpls.parameters.json rename to resources/logappdeploy.eventgridhandler-retweeter.parameters.json index c4c7464..4360c19 100644 --- a/resources/logappdeploy.eventgridhandler-twitter.azpls.parameters.json +++ b/resources/logappdeploy.eventgridhandler-retweeter.parameters.json @@ -4,6 +4,12 @@ "parameters": { "name": { "value": "" + }, + "twitterProfileId": { + "value": "" + }, + "retweeterProfileId": { + "value": "" } } } diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep b/resources/logappdeploy.eventgridhandler-twitter.bicep similarity index 70% rename from resources/logappdeploy.eventgridhandler-twitter.azpls.bicep rename to resources/logappdeploy.eventgridhandler-twitter.bicep index d18ee4f..5678ebe 100644 --- a/resources/logappdeploy.eventgridhandler-twitter.azpls.bicep +++ b/resources/logappdeploy.eventgridhandler-twitter.bicep @@ -17,13 +17,17 @@ param location string = resourceGroup().location // Resource location code param locationCode string = 'krc' -// Logic Apps -param logicAppAcceptedEventType string = 'com.youtube.video.published' -param logicAppAcceptedTitleSegment string = '애저듣보잡' - // Function App param functionName string = 'FetchAsync' +// YouTube +param youTubeAcceptedTitleSegment string +param youTubeAcceptedEventType string = 'com.youtube.video.published' + +// Twitter +param twitterProfileId string +param twitterEventType string = 'com.twitter.tweet.posted' + var metadata = { longName: '{0}-${name}-${env}-${locationCode}{1}' shortName: '{0}${name}${env}${locationCode}' @@ -31,8 +35,8 @@ var metadata = { var twitterConnector = { id: '${subscription().id}/providers/Microsoft.Web/locations/${location}/managedApis/twitter' - connectionId: '${resourceGroup().id}/providers/Microsoft.Web/connections/${format(metadata.longName, 'apicon', '-twitter-azpls')}' - connectionName: format(metadata.longName, 'apicon', '-twitter-azpls') + connectionId: '${resourceGroup().id}/providers/Microsoft.Web/connections/${format(format(metadata.longName, 'apicon', '-twitter-{0}'), twitterProfileId)}' + connectionName: format(format(metadata.longName, 'apicon', '-twitter-{0}'), twitterProfileId) location: location } @@ -49,10 +53,8 @@ resource apiconTwitter 'Microsoft.Web/connections@2016-06-01' = { } var logicApp = { - name: format(metadata.longName, 'logapp', '-eventgrid-sub-handler-twitter') + name: format(format(metadata.longName, 'logapp', '-eventgrid-sub-handler-twitter-{0}'), twitterProfileId) location: location - acceptedEventType: logicAppAcceptedEventType - acceptedTitleSegment: logicAppAcceptedTitleSegment } var functionApp = { @@ -60,6 +62,21 @@ var functionApp = { functionResourceId: resourceId('Microsoft.Web/sites/functions', format(metadata.longName, 'fncapp', ''), functionName) } +var eventGridTopic = { + name: format(metadata.longName, 'evtgrd', '-topic') + resourceId: resourceId('Microsoft.EventGrid/topics', format(metadata.longName, 'evtgrd', '-topic')) +} + +var youtube = { + acceptedEventType: youTubeAcceptedEventType + acceptedTitleSegment: youTubeAcceptedTitleSegment +} + +var twitter = { + source: 'https://twitter.com/${twitterProfileId}' + type: twitterEventType +} + resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { name: logicApp.name location: logicApp.location @@ -78,6 +95,12 @@ resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { functionAppKey: { value: listKeys(functionApp.functionResourceId, '2020-06-01').default } + eventGridTopicEndpoint: { + value: reference(eventGridTopic.resourceId, '2020-06-01', 'Full').properties.endpoint + } + eventGridTopicKey: { + value: listKeys(eventGridTopic.resourceId, '2020-06-01').key1 + } } definition: { '$schema': 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#' @@ -87,10 +110,6 @@ resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { type: 'object' defaultValue: {} } - acceptedEventType: { - type: 'string' - defaultValue: logicApp.acceptedEventType - } functionAppName: { type: 'string' defaultValue: functionApp.name @@ -101,7 +120,27 @@ resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { } acceptedTitleSegment: { type: 'string' - defaultValue: logicApp.acceptedTitleSegment + defaultValue: youtube.acceptedTitleSegment + } + acceptedEventType: { + type: 'string' + defaultValue: youtube.acceptedEventType + } + eventGridTopicEndpoint: { + type: 'string' + defaultValue: '' + } + eventGridTopicKey: { + type: 'string' + defaultValue: '' + } + twitterSource: { + type: 'string' + defaultValue: twitter.source + } + twitterType: { + type: 'string' + defaultValue: twitter.type } } triggers: { @@ -258,6 +297,45 @@ resource logapp 'Microsoft.Logic/workflows@2019-05-01' = { } } } + Build_CloudEvents_Payload: { + type: 'Compose' + runAfter: { + Post_Tweet: [ + 'Succeeded' + ] + } + inputs: { + id: '@guid()' + specversion: '1.0' + source: '@parameters(\'twitterSource\')' + type: '@parameters(\'twitterType\')' + time: '@utcNow()' + datacontenttype: 'application/cloudevents+json' + data: '@body(\'Post_Tweet\')' + } + } + Send_EventGrid_Tweet: { + type: 'Http' + runAfter: { + Build_CloudEvents_Payload: [ + 'Succeeded' + ] + } + inputs: { + method: 'POST' + uri: '@parameters(\'eventGridTopicEndpoint\')' + headers: { + 'aeg-sas-key': '@parameters(\'eventGridTopicKey\')' + 'Content-Type': '@outputs(\'Build_CloudEvents_Payload\')?[\'datacontenttype\']' + 'ce-id': '@outputs(\'Build_CloudEvents_Payload\')?[\'id\']' + 'ce-specversion': '@outputs(\'Build_CloudEvents_Payload\')?[\'specversion\']' + 'ce-source': '@outputs(\'Build_CloudEvents_Payload\')?[\'source\']' + 'ce-type': '@outputs(\'Build_CloudEvents_Payload\')?[\'type\']' + 'ce-time': '@outputs(\'Build_CloudEvents_Payload\')?[\'time\']' + } + body: '@outputs(\'Build_CloudEvents_Payload\')' + } + } } outputs: {} } diff --git a/resources/logappdeploy.eventgridhandler-twitter.azpls.json b/resources/logappdeploy.eventgridhandler-twitter.json similarity index 69% rename from resources/logappdeploy.eventgridhandler-twitter.azpls.json rename to resources/logappdeploy.eventgridhandler-twitter.json index 1b7aa74..24b69b1 100644 --- a/resources/logappdeploy.eventgridhandler-twitter.azpls.json +++ b/resources/logappdeploy.eventgridhandler-twitter.json @@ -22,17 +22,23 @@ "type": "string", "defaultValue": "krc" }, - "logicAppAcceptedEventType": { + "functionName": { "type": "string", - "defaultValue": "com.youtube.video.published" + "defaultValue": "FetchAsync" }, - "logicAppAcceptedTitleSegment": { + "youTubeAcceptedTitleSegment": { + "type": "string" + }, + "youTubeAcceptedEventType": { "type": "string", - "defaultValue": "애저듣보잡" + "defaultValue": "com.youtube.video.published" }, - "functionName": { + "twitterProfileId": { + "type": "string" + }, + "twitterEventType": { "type": "string", - "defaultValue": "FetchAsync" + "defaultValue": "com.twitter.tweet.posted" } }, "functions": [], @@ -43,19 +49,29 @@ }, "twitterConnector": { "id": "[format('{0}/providers/Microsoft.Web/locations/{1}/managedApis/twitter', subscription().id, parameters('location'))]", - "connectionId": "[format('{0}/providers/Microsoft.Web/connections/{1}', resourceGroup().id, format(variables('metadata').longName, 'apicon', '-twitter-azpls'))]", - "connectionName": "[format(variables('metadata').longName, 'apicon', '-twitter-azpls')]", + "connectionId": "[format('{0}/providers/Microsoft.Web/connections/{1}', resourceGroup().id, format(format(variables('metadata').longName, 'apicon', '-twitter-{0}'), parameters('twitterProfileId')))]", + "connectionName": "[format(format(variables('metadata').longName, 'apicon', '-twitter-{0}'), parameters('twitterProfileId'))]", "location": "[parameters('location')]" }, "logicApp": { - "name": "[format(variables('metadata').longName, 'logapp', '-eventgrid-sub-handler-twitter')]", - "location": "[parameters('location')]", - "acceptedEventType": "[parameters('logicAppAcceptedEventType')]", - "acceptedTitleSegment": "[parameters('logicAppAcceptedTitleSegment')]" + "name": "[format(format(variables('metadata').longName, 'logapp', '-eventgrid-sub-handler-twitter-{0}'), parameters('twitterProfileId'))]", + "location": "[parameters('location')]" }, "functionApp": { "name": "[format(variables('metadata').longName, 'fncapp', '')]", "functionResourceId": "[resourceId('Microsoft.Web/sites/functions', format(variables('metadata').longName, 'fncapp', ''), parameters('functionName'))]" + }, + "eventGridTopic": { + "name": "[format(variables('metadata').longName, 'evtgrd', '-topic')]", + "resourceId": "[resourceId('Microsoft.EventGrid/topics', format(variables('metadata').longName, 'evtgrd', '-topic'))]" + }, + "youtube": { + "acceptedEventType": "[parameters('youTubeAcceptedEventType')]", + "acceptedTitleSegment": "[parameters('youTubeAcceptedTitleSegment')]" + }, + "twitter": { + "source": "[format('https://twitter.com/{0}', parameters('twitterProfileId'))]", + "type": "[parameters('twitterEventType')]" } }, "resources": [ @@ -91,6 +107,12 @@ }, "functionAppKey": { "value": "[listKeys(variables('functionApp').functionResourceId, '2020-06-01').default]" + }, + "eventGridTopicEndpoint": { + "value": "[reference(variables('eventGridTopic').resourceId, '2020-06-01', 'Full').properties.endpoint]" + }, + "eventGridTopicKey": { + "value": "[listKeys(variables('eventGridTopic').resourceId, '2020-06-01').key1]" } }, "definition": { @@ -101,10 +123,6 @@ "type": "object", "defaultValue": {} }, - "acceptedEventType": { - "type": "string", - "defaultValue": "[variables('logicApp').acceptedEventType]" - }, "functionAppName": { "type": "string", "defaultValue": "[variables('functionApp').name]" @@ -115,7 +133,27 @@ }, "acceptedTitleSegment": { "type": "string", - "defaultValue": "[variables('logicApp').acceptedTitleSegment]" + "defaultValue": "[variables('youtube').acceptedTitleSegment]" + }, + "acceptedEventType": { + "type": "string", + "defaultValue": "[variables('youtube').acceptedEventType]" + }, + "eventGridTopicEndpoint": { + "type": "string", + "defaultValue": "" + }, + "eventGridTopicKey": { + "type": "string", + "defaultValue": "" + }, + "twitterSource": { + "type": "string", + "defaultValue": "[variables('twitter').source]" + }, + "twitterType": { + "type": "string", + "defaultValue": "[variables('twitter').type]" } }, "triggers": { @@ -271,6 +309,45 @@ "tweetText": "@{outputs('Build_Tweet_Post')}" } } + }, + "Build_CloudEvents_Payload": { + "type": "Compose", + "runAfter": { + "Post_Tweet": [ + "Succeeded" + ] + }, + "inputs": { + "id": "@guid()", + "specversion": "1.0", + "source": "@parameters('twitterSource')", + "type": "@parameters('twitterType')", + "time": "@utcNow()", + "datacontenttype": "application/cloudevents+json", + "data": "@body('Post_Tweet')" + } + }, + "Send_EventGrid_Tweet": { + "type": "Http", + "runAfter": { + "Build_CloudEvents_Payload": [ + "Succeeded" + ] + }, + "inputs": { + "method": "POST", + "uri": "@parameters('eventGridTopicEndpoint')", + "headers": { + "aeg-sas-key": "@parameters('eventGridTopicKey')", + "Content-Type": "@outputs('Build_CloudEvents_Payload')?['datacontenttype']", + "ce-id": "@outputs('Build_CloudEvents_Payload')?['id']", + "ce-specversion": "@outputs('Build_CloudEvents_Payload')?['specversion']", + "ce-source": "@outputs('Build_CloudEvents_Payload')?['source']", + "ce-type": "@outputs('Build_CloudEvents_Payload')?['type']", + "ce-time": "@outputs('Build_CloudEvents_Payload')?['time']" + }, + "body": "@outputs('Build_CloudEvents_Payload')" + } } }, "outputs": {} diff --git a/resources/logappdeploy.eventgridhandler-twitter.parameters.json b/resources/logappdeploy.eventgridhandler-twitter.parameters.json new file mode 100644 index 0000000..115e7e1 --- /dev/null +++ b/resources/logappdeploy.eventgridhandler-twitter.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "value": "" + }, + "youTubeAcceptedTitleSegment": { + "value": "" + }, + "twitterProfileUri": { + "value": "" + } + } +} From 614bde625f848705d77692ef032831fc59d6ff7f Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 3 Jan 2021 21:22:13 +0900 Subject: [PATCH 21/21] Update workflow --- .github/workflows/dev.yaml | 4 ++-- .github/workflows/feature.yaml | 4 ++-- .github/workflows/release.yaml | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml index 2454a86..4e4f92a 100644 --- a/.github/workflows/dev.yaml +++ b/.github/workflows/dev.yaml @@ -190,7 +190,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.eventgridhandler-retweeter.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWEETER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -235,7 +235,7 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWEETER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \ diff --git a/.github/workflows/feature.yaml b/.github/workflows/feature.yaml index 68698c9..fe38202 100644 --- a/.github/workflows/feature.yaml +++ b/.github/workflows/feature.yaml @@ -191,7 +191,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.eventgridhandler-retweeter.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWEETER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -236,7 +236,7 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWEETER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d26cc60..ef2d207 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -244,7 +244,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_DEV }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.eventgridhandler-retweeter.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_DEV }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWEETER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -289,7 +289,7 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWEETER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_DEV }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \ @@ -437,7 +437,7 @@ jobs: resourceGroupName: ${{ secrets.RESOURCE_GROUP_NAME_PROD }} deploymentName: 'ytwebsub' template: 'resources/logappdeploy.eventgridhandler-retweeter.json' - parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWITTER_PROFILE_ID }} + parameters: name=${{ secrets.RESOURCE_NAME }} env=${{ secrets.RESOURCE_ENVIRONMENT_PROD }} locationCode=${{ secrets.RESOURCE_LOCATION_CODE }} twitterEventType=${{ secrets.ACCEPTED_EVENT_TYPE_TWITTER }} twitterProfileId=${{ secrets.TWITTER_PROFILE_ID }} retweeterProfileId=${{ secrets.RETWEETER_PROFILE_ID }} - name: Download EventGrid name uses: actions/download-artifact@v2 @@ -482,7 +482,7 @@ jobs: az extension add -n eventgrid az extension add -n logic sub=$(az eventgrid event-subscription create \ - -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWITTER_PROFILE_ID}} \ + -n ${{ steps.eventgrid.outputs.name }}-sub-retweeter-${{ secrets.RETWEETER_PROFILE_ID}} \ --source-resource-id $(az eventgrid topic show \ -g ${{ secrets.RESOURCE_GROUP_NAME_PROD }} \ -n ${{ steps.eventgrid.outputs.name }}-topic \