diff --git a/configure.ac b/configure.ac index df4623d316b..bd20009e6ee 100644 --- a/configure.ac +++ b/configure.ac @@ -1818,6 +1818,10 @@ AC_LANG_POP([C++]) # Right now, the healthcheck plugins requires inotify_init (and friends) AM_CONDITIONAL([BUILD_HEALTHCHECK_PLUGIN], [ test "$ac_cv_func_inotify_init" = "yes" ]) +use_inotify=0 +AS_IF([test "x$ac_cv_func_inotify_init" = "xyes"], [use_inotify=1]) +AC_SUBST(use_inotify) + # # Check for tcmalloc, jemalloc and mimalloc TS_CHECK_TCMALLOC @@ -2262,7 +2266,8 @@ iocore_include_dirs="\ -I\$(abs_top_srcdir)/iocore/hostdb \ -I\$(abs_top_srcdir)/iocore/cache \ -I\$(abs_top_srcdir)/iocore/utils \ --I\$(abs_top_srcdir)/iocore/dns" +-I\$(abs_top_srcdir)/iocore/dns \ +-I\$(abs_top_srcdir)/iocore/fs" AC_SUBST([AM_CPPFLAGS]) AC_SUBST([AM_CFLAGS]) @@ -2310,6 +2315,7 @@ AC_CONFIG_FILES([ iocore/cache/Makefile iocore/dns/Makefile iocore/eventsystem/Makefile + iocore/fs/Makefile iocore/hostdb/Makefile iocore/net/Makefile iocore/net/quic/Makefile diff --git a/doc/admin-guide/plugins/s3_auth.en.rst b/doc/admin-guide/plugins/s3_auth.en.rst index 87f1a92097c..23da86ed698 100644 --- a/doc/admin-guide/plugins/s3_auth.en.rst +++ b/doc/admin-guide/plugins/s3_auth.en.rst @@ -44,7 +44,7 @@ Alternatively, you can store the access key and secret in an external configurat # remap.config - ... @plugin=s3_auth.so @pparam=--config @pparam=s3_auth_v2.config + ... @plugin=s3_auth.so @pparam=--config @pparam=s3_auth_v2.config @pparam=--watch-config @pparam=--ttl=5 Where ``s3.config`` could look like:: @@ -94,6 +94,8 @@ The ``s3_auth_v4.config`` config file could look like this:: v4-include-headers= v4-exclude-headers= v4-region-map=region_map.config + watch-config + ttl=20 Where the ``region_map.config`` defines the entry-point hostname to region mapping i.e.:: @@ -123,6 +125,10 @@ If ``--v4-include-headers`` is not specified all headers except those specified If ``--v4-include-headers`` is specified only the headers specified will be signed except those specified in ``--v4-exclude-headers`` +If ``--watch-config`` is specified, the plugin will reload the config file set in ``--config`` when it changes + +If ``--ttl`` is specified, the plugin will cache configs for the specified number of seconds. During the ttl period, manual config reloads and ``--watch-config`` will not cause the config to be updated. The default is 60 seconds. Setting ttl to zero causes all reloads to read from the config file. This option is useful if the config file is fetched from a service, and you wish to limit the fetch rate. + AWS Authentication version 2 ============================ diff --git a/doc/developer-guide/api/functions/TSFileEventRegister.en.rst b/doc/developer-guide/api/functions/TSFileEventRegister.en.rst new file mode 100644 index 00000000000..5e3f611fb3a --- /dev/null +++ b/doc/developer-guide/api/functions/TSFileEventRegister.en.rst @@ -0,0 +1,85 @@ +.. Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed + with this work for additional information regarding copyright + ownership. The ASF licenses this file to you under the Apache + License, Version 2.0 (the "License"); you may not use this file + except in compliance with the License. You may obtain a copy of + the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. + +.. include:: ../../../common.defs + +.. default-domain:: c + +TSFileEventRegister +******************* + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. function:: TSWatchDescriptor TSFileEventRegister(const char *path, TSFileWatchKind kind, TSCont contp) + +Description +=========== + +Attempt to register a watch on a file or directory. :arg:`contp` will be called when +the OS reports a change in the file system at :arg:`path`. + +Types +===== + +.. enum:: TSFileWatchKind + + The kind of changes to watch on the path. + + .. enumerator:: TS_WATCH_CREATE + + Only valid on directories. :arg:`contp` is called after a file or directory has been + created under :arg:`path`. Some operating systems, such as Linux, will supply a file + name of the newly created file or directory. This name is passed to :arg:`contp` when + it's available. + + .. enumerator:: TS_WATCH_DELETE + + Valid on files and directories. :arg:`contp` is called after :arg:`path` is deleted. + + .. enumerator:: TS_WATCH_MODIFY + + Valid on files and directories. :arg:`contp` is called after :arg:`path` has been modified. + +.. struct:: TSFileWatchData + + A class that holds information for the callback. :arg:`contp` will be called back with + :arg:`edata` pointing to one of these. + + .. member:: TSWatchDescriptor wd + + The watch descriptor for that was previously returned from :func:`TSFileEventRegister`. + + .. member:: const char *name + + Only sometimes populated for :enumerator:`TS_WATCH_CREATE`. The name of the created + file. Note that this name is not always available. When it's unavailable, the value will + be :code:`nullptr`. + +.. type:: TSWatchDescriptor + + An opaque type that identifies a file system watch. + +Return Value +============ + +Returns a TSWatchDescriptor on success, or -1 on failure. The caller should store the +returned watch descriptor . + diff --git a/doc/developer-guide/api/functions/TSFileEventUnregister.en.rst b/doc/developer-guide/api/functions/TSFileEventUnregister.en.rst new file mode 100644 index 00000000000..5803a4d8667 --- /dev/null +++ b/doc/developer-guide/api/functions/TSFileEventUnregister.en.rst @@ -0,0 +1,37 @@ +.. Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed + with this work for additional information regarding copyright + ownership. The ASF licenses this file to you under the Apache + License, Version 2.0 (the "License"); you may not use this file + except in compliance with the License. You may obtain a copy of + the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. + +.. include:: ../../../common.defs + +.. default-domain:: c + +TSFileEventUnregister +********************* + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. function:: void TSFileEventUnregister(TSWatchDescriptor wd) + +Description +=========== + +Unregister a watch that was registered by :func:`TSFileEventRegister`. :arg:`wd` is the return value from :func:`TSFileEventRegister`. + diff --git a/doc/developer-guide/testing/blackbox-testing.en.rst b/doc/developer-guide/testing/blackbox-testing.en.rst index 25af57aec31..5ca6f3f579a 100644 --- a/doc/developer-guide/testing/blackbox-testing.en.rst +++ b/doc/developer-guide/testing/blackbox-testing.en.rst @@ -262,6 +262,7 @@ Condition Testing - TS_HAS_128BIT_CAS - TS_HAS_TESTS - TS_HAS_WCCP + - TS_USE_INOTIFY Examples: diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 9baae2bfda0..faf475875d4 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -554,7 +554,12 @@ typedef enum { TS_EVENT_SSL_CLIENT_HELLO = 60207, TS_EVENT_SSL_SECRET = 60208, - TS_EVENT_MGMT_UPDATE = 60300 + TS_EVENT_MGMT_UPDATE = 60300, + + TS_EVENT_FILE_CREATED = 60400, + TS_EVENT_FILE_UPDATED = 60401, + TS_EVENT_FILE_DELETED = 60402, + TS_EVENT_FILE_IGNORED = 60403 } TSEvent; #define TS_EVENT_HTTP_READ_REQUEST_PRE_REMAP TS_EVENT_HTTP_PRE_REMAP /* backwards compat */ @@ -1460,6 +1465,13 @@ namespace ts } #endif +typedef int TSWatchDescriptor; +typedef enum { TS_WATCH_CREATE, TS_WATCH_DELETE, TS_WATCH_MODIFY } TSFileWatchKind; +typedef struct { + TSWatchDescriptor wd; + const char *name; +} TSFileWatchData; + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/include/ts/ts.h b/include/ts/ts.h index 66af0983da1..7af5339c155 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -2755,6 +2755,21 @@ tsapi void TSHostStatusSet(const char *hostname, const size_t hostname_len, TSHo tsapi bool TSHttpTxnCntlGet(TSHttpTxn txnp, TSHttpCntlType ctrl); tsapi TSReturnCode TSHttpTxnCntlSet(TSHttpTxn txnp, TSHttpCntlType ctrl, bool data); +/* + * Get notified for file system events + * + * TODO: Fix multiple plugins watching the same path. + * + * The edata (a.k.a. cookie) field of the continuation handler will contain information + * depending on the type of file event. edata is always a pointer to a TSFileWatchData. + * If the event is TS_EVENT_FILE_CREATED, and ATS is running on Linux, name is a pointer + * to a null-terminated string containing the file name. Otherwise, name is a nullptr. + * wd is the watch descriptor for the event. + * + */ +tsapi TSWatchDescriptor TSFileEventRegister(const char *path, TSFileWatchKind kind, TSCont contp); +tsapi void TSFileEventUnregister(TSWatchDescriptor wd); + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/include/tscore/ink_config.h.in b/include/tscore/ink_config.h.in index 5b297efe7bf..b6e635f4f31 100644 --- a/include/tscore/ink_config.h.in +++ b/include/tscore/ink_config.h.in @@ -83,6 +83,7 @@ #define TS_HAS_TLS_EARLY_DATA @has_tls_early_data@ #define TS_HAS_TLS_SESSION_TICKET @has_tls_session_ticket@ #define TS_HAS_VERIFY_CERT_STORE @has_verify_cert_store@ +#define TS_USE_INOTIFY @use_inotify@ #define TS_USE_HRW_GEOIP @use_hrw_geoip@ #define TS_USE_HRW_MAXMINDDB @use_hrw_maxminddb@ diff --git a/iocore/Makefile.am b/iocore/Makefile.am index 5aae15ea552..f870d193ba5 100644 --- a/iocore/Makefile.am +++ b/iocore/Makefile.am @@ -16,4 +16,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -SUBDIRS = eventsystem net aio dns hostdb utils cache +SUBDIRS = eventsystem net aio dns hostdb utils cache fs diff --git a/iocore/fs/FileChange.cc b/iocore/fs/FileChange.cc new file mode 100644 index 00000000000..eb742bb5bce --- /dev/null +++ b/iocore/fs/FileChange.cc @@ -0,0 +1,412 @@ +/** @file FileChange.cc + + Watch for file system changes. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "FileChange.h" +#include "tscore/Diags.h" +#include "P_EventSystem.h" +#include "tscore/ink_assert.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if TS_USE_KQUEUE +#include +#endif + +// Globals +FileChangeManager fileChangeManager; +static constexpr auto TAG ATS_UNUSED = "FileChange"; + +#if TS_USE_KQUEUE +using namespace std::chrono_literals; + +// When using kqueue, new watches will take effect after at most this much time. +// This value should be greater than 0. Smaller values cost more CPU. +static constexpr auto latency = 1s; + +static constexpr timespec +chrono_to_timespec(std::chrono::nanoseconds duration) +{ + if (duration <= 0s) { + duration = 1s; + } + + auto seconds = std::chrono::duration_cast(duration); + duration -= seconds; + return timespec{seconds.count(), duration.count()}; +} + +#endif + +// Wrap a continuation +class FileChangeCallback : public Continuation +{ +public: + explicit FileChangeCallback(Continuation *contp, TSEvent event) : Continuation(contp->mutex.get()), m_cont(contp), m_event(event) + { + SET_HANDLER(&FileChangeCallback::event_handler); + } + + int + event_handler(int, void *eventp) + { + Event *e = reinterpret_cast(eventp); + if (m_cont->mutex) { + MUTEX_TRY_LOCK(trylock, m_cont->mutex, this_ethread()); + if (!trylock.is_locked()) { + eventProcessor.schedule_in(this, HRTIME_MSECONDS(10), ET_TASK); + } else { + m_cont->handleEvent(m_event, e->cookie); + delete this; + } + } else { + m_cont->handleEvent(m_event, e->cookie); + delete this; + } + + return 0; + } + + std::string filename; // File name if the event is a file creation event. This is used in the cookie for a create event. + TSFileWatchData data; + +private: + Continuation *m_cont; + TSEvent m_event; +}; + +static void +invoke(FileChangeCallback *cb) +{ + void *cookie = static_cast(&cb->data); + eventProcessor.schedule_imm(cb, ET_TASK, 1, cookie); +} + +#if TS_USE_INOTIFY +static constexpr size_t INOTIFY_BUF_SIZE = 4096; + +void +FileChangeManager::inotify_process_event(struct inotify_event *event) +{ + std::shared_lock file_watches_read_lock(file_watches_mutex); + auto finfo_it = file_watches.find(event->wd); + if (finfo_it != file_watches.end()) { + TSEvent event_type = TS_EVENT_NONE; + const FileChangeInfo &finfo = finfo_it->second; + Continuation *contp = finfo.contp; + + if (event->mask & (IN_DELETE_SELF | IN_MOVED_FROM)) { + Debug(TAG, "Delete file event (%d) on %s", event->mask, finfo.path.c_str()); + event_type = TS_EVENT_FILE_DELETED; + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->data.wd = event->wd; + cb->data.name = nullptr; + invoke(cb); + } + + if (event->mask & (IN_CREATE | IN_MOVED_TO)) { + // Name may be padded with nul characters. Trim them. + auto len = strnlen(event->name, event->len); + std::string name{event->name, len}; + Debug(TAG, "Create file event (%d) on %s (wd = %d): %s", event->mask, finfo.path.c_str(), event->wd, name.c_str()); + event_type = TS_EVENT_FILE_CREATED; + + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->filename = name; + cb->data.wd = event->wd; + cb->data.name = cb->filename.c_str(); + invoke(cb); + } + + if (event->mask & (IN_CLOSE_WRITE | IN_ATTRIB)) { + Debug(TAG, "Modify file event (%d) on %s (wd = %d)", event->mask, finfo.path.c_str(), event->wd); + event_type = TS_EVENT_FILE_UPDATED; + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->data.wd = event->wd; + cb->data.name = nullptr; + invoke(cb); + } + + if (event->mask & (IN_IGNORED)) { + Debug(TAG, "Ignored file event (%d) on %s (wd = %d)", event->mask, finfo.path.c_str(), event->wd); + event_type = TS_EVENT_FILE_IGNORED; + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->data.wd = event->wd; + cb->data.name = nullptr; + invoke(cb); + } + } +} +#elif TS_USE_KQUEUE +static void +kqueue_make_event(int fd, const FileChangeInfo &info, struct kevent *event) +{ + unsigned int mask = 0; + if (info.kind == TS_WATCH_CREATE) { + mask = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME; + } else if (info.kind == TS_WATCH_DELETE) { + mask = NOTE_DELETE | NOTE_RENAME; + } else if (info.kind == TS_WATCH_MODIFY) { + mask = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME; + } else { + // Shouldn't get here + ink_release_assert(false); + } + EV_SET(event, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, mask, 0, (void *)(uintptr_t)fd); +} + +void +FileChangeManager::kqueue_prepare_events() +{ + if (file_watches_dirty) { + // Make events for kqueue to monitor + Debug(TAG, "Updating kqueue event list."); + std::unique_lock file_watches_lock{file_watches_mutex}; // unique lock because file_watches_dirty will be written + auto nwatches = file_watches.size(); + events_to_monitor.resize(nwatches); + events_from_kqueue.resize(nwatches); + int i = 0; + for (const auto &[wd, info] : file_watches) { + // Add a file event to the list of events to monitor + kqueue_make_event(wd, info, &events_to_monitor[i]); + i++; + } + file_watches_dirty = false; + } +} + +int +FileChangeManager::kqueue_wait_for_events() +{ + // Wait for kqueue events + if (events_to_monitor.empty()) { + std::this_thread::sleep_for(latency); + } else { + // I couldn't find a clear answer as to whether the timeout value can be modified by kevent (e.g. on an interrupt) + constexpr timespec latency_timespec = chrono_to_timespec(latency); + return kevent(kq, events_to_monitor.data(), events_to_monitor.size(), events_from_kqueue.data(), events_from_kqueue.size(), + &latency_timespec); + } + + return 0; +} + +void +FileChangeManager::kqueue_process_event(const struct kevent &event) +{ + std::shared_lock file_watches_read_lock(file_watches_mutex); + auto fd64 = reinterpret_cast(event.udata); + auto fd = static_cast(fd64); // Intentionally truncating to an int. Casting twice to suppress compiler warnings. + auto finfo_it = file_watches.find(fd); + if (finfo_it != file_watches.end()) { + TSEvent event_type = TS_EVENT_NONE; + const FileChangeInfo &finfo = finfo_it->second; + Continuation *contp = finfo.contp; + + if (event.fflags & (NOTE_DELETE | NOTE_RENAME)) { + Debug(TAG, "Delete file event (%d) on %s", event.fflags, finfo.path.c_str()); + if (finfo.kind == TS_WATCH_DELETE) { + event_type = TS_EVENT_FILE_DELETED; + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->data.wd = fd; + cb->data.name = nullptr; + invoke(cb); + } + + // kqueue doesn't notify us if a file watch no longer applies, so we do it. + event_type = TS_EVENT_FILE_IGNORED; + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->data.wd = fd; + cb->data.name = nullptr; + invoke(cb); + } + + if (event.fflags & (NOTE_WRITE) && finfo.kind == TS_WATCH_CREATE) { + Debug(TAG, "Create file event (%d) on %s (wd = %d)", event.fflags, finfo.path.c_str(), fd); + event_type = TS_EVENT_FILE_CREATED; + + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->data.wd = fd; + cb->data.name = nullptr; // a kqueue create has no name + invoke(cb); + } + + if (event.fflags & (NOTE_WRITE) && finfo.kind == TS_WATCH_MODIFY) { + Debug(TAG, "Modify file event (%d) on %s (wd = %d)", event.fflags, finfo.path.c_str(), fd); + event_type = TS_EVENT_FILE_UPDATED; + FileChangeCallback *cb = new FileChangeCallback(contp, event_type); + cb->data.wd = fd; + cb->data.name = nullptr; + invoke(cb); + } + } + if (event.flags & EV_ERROR) { + Error("kqueue error: %s (%" PRIxPTR ")", strerror(event.data), event.data); + return; + } +} + +#endif + +void +FileChangeManager::init() +{ +#if TS_USE_INOTIFY + // TODO: auto configure based on whether inotify is available + inotify_fd = inotify_init1(IN_CLOEXEC); + if (inotify_fd == -1) { + Error("Failed to init inotify: %s (%d)", strerror(errno), errno); + return; + } + auto inotify_thread = [manager = this]() mutable { + for (;;) { + char inotify_buf[INOTIFY_BUF_SIZE]; + + // blocking read + ssize_t rc = read(manager->inotify_fd, inotify_buf, sizeof inotify_buf); + + if (rc == -1) { + Error("Failed to read inotify: %s (%d)", strerror(errno), errno); + if (errno == EINTR) { + continue; + } else { + break; + } + } + + ssize_t offset = 0; + while (offset < rc) { + struct inotify_event *event = reinterpret_cast(inotify_buf + offset); + + // Process file events + manager->inotify_process_event(event); + offset += sizeof(struct inotify_event) + event->len; + } + } + }; + poll_thread = std::thread(inotify_thread); + poll_thread.detach(); +#elif TS_USE_KQUEUE + auto kqueue_thread = [manager = this]() mutable { + manager->kq = kqueue(); + if (manager->kq < 0) { + Fatal("Failed to init kqueue: %s.", strerror(errno)); + } + for (;;) { + manager->kqueue_prepare_events(); + int event_count = manager->kqueue_wait_for_events(); + if (event_count == -1) { + Error("kqueue error: %s", strerror(errno)); + } + for (int i = 0; i < event_count; i++) { + manager->kqueue_process_event(manager->events_from_kqueue[i]); + } + } + }; + poll_thread = std::thread(kqueue_thread); + poll_thread.detach(); +#else + // Implement this +#endif +} + +watch_handle_t +FileChangeManager::add(const ts::file::path &path, TSFileWatchKind kind, Continuation *contp) +{ + watch_handle_t wd = 0; + std::unique_lock file_watches_write_lock{file_watches_mutex}; + Debug(TAG, "Adding a watch on %s", path.c_str()); + +#if TS_USE_INOTIFY + // Let the OS handle multiple watches on one file. + uint32_t mask = 0; + if (kind == TS_WATCH_CREATE) { + mask = IN_CREATE | IN_MOVED_TO | IN_ONLYDIR; + } else if (kind == TS_WATCH_DELETE) { + mask = IN_DELETE_SELF | IN_MOVED_FROM; + } else if (kind == TS_WATCH_MODIFY) { + mask = IN_CLOSE_WRITE | IN_ATTRIB; + } else { + // Shouldn't get here + ink_release_assert(false); + } + wd = inotify_add_watch(inotify_fd, path.c_str(), mask); + if (wd == -1) { + Error("Failed to add file watch on %s: %s (%d)", path.c_str(), strerror(errno), errno); + return -1; + } + + Debug(TAG, "Watch handle = %d", wd); +#elif TS_USE_KQUEUE + int o_flags = 0; + +#ifdef O_SYMLINK + o_flags |= O_SYMLINK; +#endif + +#ifdef O_EVTONLY + // The descriptor is requested for event notifications only. + o_flags |= O_EVTONLY; +#else + o_flags |= O_RDONLY; +#endif + +#ifdef O_DIRECTORY + if (kind == TS_WATCH_CREATE) { + // file creation can only be monitored from the directory above + o_flags |= O_DIRECTORY; + } +#endif + + wd = open(path.c_str(), o_flags); + if (wd <= 0) { + Error("Failed to open %s for monitoring: %s.", path.c_str(), strerror(errno)); + return -1; + } + file_watches_dirty = true; +#else + Warning("File change notification is not supported on this OS."); +#endif + file_watches.try_emplace(wd, kind, path, contp); + return wd; +} + +void +FileChangeManager::remove(watch_handle_t watch_handle) +{ + std::unique_lock file_watches_write_lock(file_watches_mutex); + Debug(TAG, "Deleting watch %d", watch_handle); +#if TS_USE_INOTIFY + inotify_rm_watch(inotify_fd, watch_handle); +#elif TS_USE_KQUEUE + close(watch_handle); + file_watches_dirty = true; +#endif + file_watches.erase(watch_handle); +} diff --git a/iocore/fs/FileChange.h b/iocore/fs/FileChange.h new file mode 100644 index 00000000000..1437c0d36e9 --- /dev/null +++ b/iocore/fs/FileChange.h @@ -0,0 +1,103 @@ +/** @file FileChange.h + + Watch for file system changes. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#pragma once +#include "ts/apidefs.h" +#include "tscore/ink_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include "tscore/ts_file.h" +#include "P_EventSystem.h" +#include +#include + +#if TS_USE_INOTIFY +#include +#else +// implement this +#endif + +using watch_handle_t = int; + +// File watch info +class FileChangeInfo +{ +public: + FileChangeInfo(TSFileWatchKind kind, ts::file::path path, Continuation *contp) : kind{kind}, path{std::move(path)}, contp{contp} + { + } + + TSFileWatchKind kind; + ts::file::path path; + Continuation *contp; +}; + +class FileChangeManager +{ +public: + FileChangeManager() {} + + void init(); + + /** + Add a file watch + + @return a watch handle, or -1 on error + */ + watch_handle_t add(const ts::file::path &path, TSFileWatchKind kind, Continuation *contp); + + /** + Remove a file watch + */ + void remove(watch_handle_t watch_handle); + +private: + std::thread poll_thread; + std::unordered_map file_watches; // protected by file_watches_mutex + std::shared_mutex file_watches_mutex; + +#if TS_USE_INOTIFY + void inotify_process_event(struct inotify_event *event); + int inotify_fd; +#elif TS_USE_KQUEUE + bool file_watches_dirty; // protected by file_watches_mutex + + std::vector events_to_monitor; + std::vector events_from_kqueue; + + int kq; + void kqueue_prepare_events(); + int kqueue_wait_for_events(); + void kqueue_process_event(const struct kevent &event); +#else + // implement this +#endif +}; + +extern FileChangeManager fileChangeManager; diff --git a/iocore/fs/Makefile.am b/iocore/fs/Makefile.am new file mode 100644 index 00000000000..a851273eadc --- /dev/null +++ b/iocore/fs/Makefile.am @@ -0,0 +1,34 @@ +# Makefile.am for the traffic/iocore/fs hierarchy +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +AM_CPPFLAGS += \ + $(iocore_include_dirs) \ + -I$(abs_top_srcdir)/include \ + -I$(abs_top_srcdir)/lib \ + $(TS_INCLUDES) + +noinst_LIBRARIES = libinkfs.a + +libinkfs_a_SOURCES = \ + FileChange.cc \ + FileChange.h + +include $(top_srcdir)/build/tidy.mk + +clang-tidy-local: $(DIST_SOURCES) + $(CXX_Clang_Tidy) diff --git a/plugins/s3_auth/s3_auth.cc b/plugins/s3_auth/s3_auth.cc index df5c090779c..e4f488f1bbe 100644 --- a/plugins/s3_auth/s3_auth.cc +++ b/plugins/s3_auth/s3_auth.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -42,11 +43,13 @@ #include #include #include +#include #include #include #include #include "tscore/ink_config.h" +#include "tscore/ts_file.h" #include "aws_auth_v4.h" @@ -138,53 +141,13 @@ loadRegionMap(StringMap &m, const String &filename) return true; } -/////////////////////////////////////////////////////////////////////////////// -// Cache for the secrets file, to avoid reading / loading them repeatedly on -// a reload of remap.config. This gets cached for 60s (not configurable). -// -class S3Config; - -class ConfigCache -{ -public: - S3Config *get(const char *fname); - -private: - struct _ConfigData { - // This is incremented before and after cnf and load_time are set. - // Thus, an odd value indicates an update is in progress. - std::atomic update_status{0}; - - // A config from a file and the last time it was loaded. - // config should be written before load_time. That way, - // if config is read after load_time, the load time will - // never indicate config is fresh when it isn't. - std::atomic config; - std::atomic load_time; - - _ConfigData() {} - - _ConfigData(S3Config *config_, time_t load_time_) : config(config_), load_time(load_time_) {} - - _ConfigData(_ConfigData &&lhs) - { - update_status = lhs.update_status.load(); - config = lhs.config.load(); - load_time = lhs.load_time.load(); - } - }; - - std::unordered_map _cache; - static const int _ttl = 60; -}; - -ConfigCache gConfCache; - /////////////////////////////////////////////////////////////////////////////// // One configuration setup // int event_handler(TSCont, TSEvent, void *); // Forward declaration int config_reloader(TSCont, TSEvent, void *); +int config_dir_watch(TSCont, TSEvent, void *); +int config_watch(TSCont, TSEvent, void *); class S3Config { @@ -197,6 +160,12 @@ class S3Config _conf_rld = TSContCreate(config_reloader, TSMutexCreate()); TSContDataSet(_conf_rld, static_cast(this)); + + _dir_watch = TSContCreate(config_dir_watch, TSMutexCreate()); + TSContDataSet(_dir_watch, static_cast(this)); + + _conf_watch = TSContCreate(config_watch, TSMutexCreate()); + TSContDataSet(_conf_watch, static_cast(this)); } } @@ -209,6 +178,7 @@ class S3Config TSfree(_conf_fname); if (_conf_rld_act) { TSActionCancel(_conf_rld_act); + _conf_rld_act = nullptr; } if (_conf_rld) { TSContDestroy(_conf_rld); @@ -216,6 +186,14 @@ class S3Config if (_cont) { TSContDestroy(_cont); } + + if (_dir_watch) { + TSContDestroy(_dir_watch); + } + + if (_conf_watch) { + TSContDestroy(_conf_watch); + } } // Is this configuration usable? @@ -302,6 +280,10 @@ class S3Config TSfree(_conf_fname); _conf_fname = TSstrdup(src->_conf_fname); } + + if (src->_watch_config) { + _watch_config = src->_watch_config; + } } // Getters @@ -383,6 +365,12 @@ class S3Config return _conf_fname; } + bool + watch_config() const + { + return _watch_config; + } + int incr_conf_reload_count() { @@ -463,12 +451,24 @@ class S3Config _conf_fname = TSstrdup(s); } + void + set_watch_config() + { + _watch_config = true; + } + void reset_conf_reload_count() { _conf_reload_count = 0; } + void + clear_reload_action() + { + _conf_rld_act = nullptr; + } + // Parse configs from an external file bool parse_config(const std::string &filename); @@ -484,12 +484,89 @@ class S3Config schedule_conf_reload(long delay) { if (_conf_rld_act != nullptr && !TSActionDone(_conf_rld_act)) { + TSDebug(PLUGIN_NAME, "_conf_rld_act = %p, cancelling...", _conf_rld_act); TSActionCancel(_conf_rld_act); + _conf_rld_act = nullptr; } _conf_rld_act = TSContScheduleOnPool(_conf_rld, delay * 1000, TS_THREAD_POOL_NET); } + void + start_watch_config() + { + std::unique_lock lock(wd_mutex); + ts::file::path fname{makeConfigPath(_conf_fname)}; + if (!_config_file_wd) { + _config_file_wd = TSFileEventRegister(fname.c_str(), TS_WATCH_MODIFY, _conf_watch); + if (_config_file_wd == -1) { + _config_file_wd.reset(); + TSDebug(PLUGIN_NAME, "Waiting for config file to be created: %s", fname.c_str()); + } else { + TSDebug(PLUGIN_NAME, "Watching config file: %s (%d)", fname.c_str(), _config_file_wd.value()); + } + } + + if (!_config_dir_wd) { + auto parent_dir = fname.parent_path(); + _config_dir_wd = TSFileEventRegister(parent_dir.c_str(), TS_WATCH_CREATE, _dir_watch); + if (_config_dir_wd == -1) { + _config_dir_wd.reset(); + TSError("[%s]: failed to watch config file directory: %s", PLUGIN_NAME, parent_dir.c_str()); + } else { + TSDebug(PLUGIN_NAME, "Watching config file directory: %s (%d)", parent_dir.c_str(), _config_file_wd.value()); + } + } + } + + void + stop_watch_config() + { + std::unique_lock lock(wd_mutex); + if (_config_file_wd) { + TSFileEventUnregister(_config_file_wd.value()); + _config_file_wd.reset(); + } + + if (_config_dir_wd) { + TSFileEventUnregister(_config_dir_wd.value()); + _config_dir_wd.reset(); + } + } + + void + config_file_watch_ignored(TSWatchDescriptor wd) + { + std::unique_lock lock(wd_mutex); + if (_config_file_wd == wd) { + TSFileEventUnregister(_config_file_wd.value()); + _config_file_wd.reset(); + } + } + + void + config_dir_watch_ignored(TSWatchDescriptor wd) + { + std::unique_lock lock(wd_mutex); + if (_config_dir_wd == wd) { + TSFileEventUnregister(_config_dir_wd.value()); + _config_dir_wd.reset(); + } + } + + void + set_watch_retry(int retries) + { + watch_retry_count = retries; + } + + int + decrement_retry() + { + return --watch_retry_count; + } + ts::shared_mutex reload_mutex; + ts::shared_mutex wd_mutex; private: char *_secret = nullptr; @@ -504,6 +581,8 @@ class S3Config bool _virt_host_modified = false; TSCont _cont = nullptr; TSCont _conf_rld = nullptr; + TSCont _conf_watch = nullptr; + TSCont _dir_watch = nullptr; TSAction _conf_rld_act = nullptr; StringSet _v4includeHeaders; bool _v4includeHeaders_modified = false; @@ -514,6 +593,10 @@ class S3Config long _expiration = 0; char *_conf_fname = nullptr; int _conf_reload_count = 0; + bool _watch_config = false; + std::optional _config_file_wd; + std::optional _config_dir_wd; + std::atomic watch_retry_count = 0; }; bool @@ -524,12 +607,14 @@ S3Config::parse_config(const std::string &config_fname) return false; } else { char line[512]; // These are long lines ... - FILE *file = fopen(config_fname.c_str(), "r"); + int fd = open(config_fname.c_str(), O_RDONLY); + FILE *file = fdopen(fd, "r"); if (nullptr == file) { TSError("[%s] unable to open %s", PLUGIN_NAME, config_fname.c_str()); return false; } + // TODO: we really should have some kind of file locking strategy here. while (fgets(line, sizeof(line), file) != nullptr) { char *pos1, *pos2; @@ -555,7 +640,7 @@ S3Config::parse_config(const std::string &config_fname) // Identify the keys (and values if appropriate) std::string key_val(pos2, pos1 - pos2 + 1); - size_t eq_pos = key_val.find_first_of("="); + size_t eq_pos = key_val.find_first_of('='); std::string key_str = trimWhiteSpaces(key_val.substr(0, eq_pos == String::npos ? key_val.size() : eq_pos)); std::string val_str = eq_pos == String::npos ? "" : trimWhiteSpaces(key_val.substr(eq_pos + 1, key_val.size())); @@ -577,6 +662,8 @@ S3Config::parse_config(const std::string &config_fname) set_region_map(val_str.c_str()); } else if (key_str == "expiration") { set_expiration(val_str.c_str()); + } else if (key_str == "watch-config") { + set_watch_config(); } else { // ToDo: warnings? } @@ -595,10 +682,10 @@ S3Config::parse_config(const std::string &config_fname) // has to copy the relevant portions, but should not use the returned object // directly (i.e. it must be copied). // -S3Config * -ConfigCache::get(const char *fname) +static S3Config * +load_config(const char *fname) { - S3Config *s3; + S3Config *s3 = nullptr; struct timeval tv; @@ -607,58 +694,17 @@ ConfigCache::get(const char *fname) // Make sure the filename is an absolute path, prepending the config dir if needed std::string config_fname = makeConfigPath(fname); - auto it = _cache.find(config_fname); - - if (it != _cache.end()) { - unsigned update_status = it->second.update_status; - if (tv.tv_sec > (it->second.load_time + _ttl)) { - if (!(update_status & 1) && it->second.update_status.compare_exchange_strong(update_status, update_status + 1)) { - TSDebug(PLUGIN_NAME, "Configuration from %s is stale, reloading", config_fname.c_str()); - s3 = new S3Config(false); // false == this config does not get the continuation - - if (s3->parse_config(config_fname)) { - s3->set_conf_fname(fname); - } else { - // Failed the configuration parse... Set the cache response to nullptr - delete s3; - s3 = nullptr; - TSAssert(!"Configuration parsing / caching failed"); - } + s3 = new S3Config(false); // false == this config does not get the continuation - delete it->second.config; - it->second.config = s3; - it->second.load_time = tv.tv_sec; - - // Update is complete. - ++it->second.update_status; - } else { - // This thread lost the race with another thread that is also reloading - // the config for this file. Wait for the other thread to finish reloading. - while (it->second.update_status & 1) { - // Hopefully yielding will sleep the thread at least until the next - // scheduler interrupt, preventing a busy wait. - std::this_thread::yield(); - } - s3 = it->second.config; - } - } else { - TSDebug(PLUGIN_NAME, "Configuration from %s is fresh, reusing", config_fname.c_str()); - s3 = it->second.config; - } + if (s3->parse_config(config_fname)) { + s3->set_conf_fname(fname); + TSDebug(PLUGIN_NAME, "Updated rule: access_key=%s, virtual_host=%s, version=%d", s3->keyid(), s3->virt_host() ? "yes" : "no", + s3->version()); } else { - // Create a new cached file. - s3 = new S3Config(false); // false == this config does not get the continuation - - TSDebug(PLUGIN_NAME, "Parsing and caching configuration from %s, version:%d", config_fname.c_str(), s3->version()); - if (s3->parse_config(config_fname)) { - s3->set_conf_fname(fname); - _cache.emplace(config_fname, _ConfigData(s3, tv.tv_sec)); - } else { - delete s3; - s3 = nullptr; - TSAssert(!"Configuration parsing / caching failed"); - } + delete s3; + s3 = nullptr; } + return s3; } @@ -1047,16 +1093,83 @@ cal_reload_delay(long time_diff) } } +int +config_dir_watch(TSCont cont, TSEvent event, void *edata) +{ + TSDebug(PLUGIN_NAME, "config directory watch handler"); + S3Config *s3 = static_cast(TSContDataGet(cont)); + TSAssert(edata != nullptr); + TSFileWatchData *fwd = reinterpret_cast(edata); + + if (event == TS_EVENT_FILE_IGNORED) { + TSDebug(PLUGIN_NAME, "Config directory lost. No longer watching for config changes."); + // Lost the config file's directory. We currently can't deal with this. Just stop watching for file changes. + s3->config_dir_watch_ignored(fwd->wd); + return TS_SUCCESS; + } + + TSAssert(event == TS_EVENT_FILE_CREATED); + // On some platforms, we get a file name on create. + if (fwd->name != nullptr) { + TSDebug(PLUGIN_NAME, "File created: %s", fwd->name); + if (strncmp(s3->conf_fname(), fwd->name, strlen(s3->conf_fname())) == 0) { + TSDebug(PLUGIN_NAME, "config file created"); + } else { + // Some other file was created. + return TS_SUCCESS; + } + } + + s3->set_watch_retry(5); + config_reloader(cont, event, edata); + return TS_SUCCESS; +} + +int +config_watch(TSCont cont, TSEvent event, void *edata) +{ + TSDebug(PLUGIN_NAME, "config watch handler"); + TSAssert(edata != nullptr); + TSFileWatchData *fwd = reinterpret_cast(edata); + + S3Config *s3 = static_cast(TSContDataGet(cont)); + if (event == TS_EVENT_FILE_IGNORED) { + TSDebug(PLUGIN_NAME, "Config file watch lost."); + // Probably deleted. Directory watch will see if it's re-created. + s3->config_file_watch_ignored(fwd->wd); + return TS_SUCCESS; + } + + s3->set_watch_retry(5); + config_reloader(cont, event, edata); + + return TS_SUCCESS; +} + int config_reloader(TSCont cont, TSEvent event, void *edata) { TSDebug(PLUGIN_NAME, "reloading configs"); - S3Config *s3 = static_cast(TSContDataGet(cont)); - S3Config *file_config = gConfCache.get(s3->conf_fname()); + S3Config *s3 = static_cast(TSContDataGet(cont)); + s3->clear_reload_action(); + S3Config *file_config = load_config(s3->conf_fname()); if (!file_config || !file_config->valid()) { - TSError("[%s] requires both shared and AWS secret configuration", PLUGIN_NAME); - return TS_ERROR; + // An invalid config is not necessarily an error. + // Sometimes a file notification is sent before a file finishes writing. + // It's also possible that the advisory lock on the file is still being held. + TSDebug(PLUGIN_NAME, "Failed to reload config."); + delete file_config; + file_config = nullptr; + auto remaining = s3->decrement_retry(); + if (remaining > 0) { + TSDebug(PLUGIN_NAME, "Retring config reload in 1 second. %d tries remaining.", remaining); + s3->schedule_conf_reload(1); + return TS_SUCCESS; + } else { + TSError("[%s] Giving up config reload. %d tries remaining.", PLUGIN_NAME, remaining); + return TS_ERROR; + } } { @@ -1064,6 +1177,9 @@ config_reloader(TSCont cont, TSEvent event, void *edata) s3->copy_changes_from(file_config); } + delete file_config; + file_config = nullptr; + if (s3->expiration() == 0) { TSDebug(PLUGIN_NAME, "disabling auto config reload"); } else { @@ -1084,6 +1200,12 @@ config_reloader(TSCont cont, TSEvent event, void *edata) } } + if (s3->watch_config()) { + s3->start_watch_config(); + } else { + s3->stop_watch_config(); + } + return TS_SUCCESS; } @@ -1124,6 +1246,7 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE {const_cast("v4-exclude-headers"), required_argument, nullptr, 'e'}, {const_cast("v4-region-map"), required_argument, nullptr, 'm'}, {const_cast("session_token"), required_argument, nullptr, 't'}, + {const_cast("watch-config"), no_argument, nullptr, 'w'}, {nullptr, no_argument, nullptr, '\0'}, }; @@ -1140,12 +1263,20 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE switch (opt) { case 'c': - file_config = gConfCache.get(optarg); // Get cached, or new, config object, from a file - if (!file_config) { + file_config = load_config(optarg); // Get cached, or new, config object, from a file + if (!file_config || !file_config->valid()) { TSError("[%s] invalid configuration file, %s", PLUGIN_NAME, optarg); - *ih = nullptr; + delete file_config; + file_config = nullptr; + *ih = nullptr; return TS_ERROR; + } else { + s3->copy_changes_from(file_config); + // Copy the config file secret into our instance of the configuration. + delete file_config; + file_config = nullptr; } + break; case 'a': s3->set_keyid(optarg); @@ -1171,6 +1302,9 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE case 'm': s3->set_region_map(optarg); break; + case 'w': + s3->set_watch_config(); + break; } if (opt == -1) { @@ -1178,11 +1312,6 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE } } - // Copy the config file secret into our instance of the configuration. - if (file_config) { - s3->copy_changes_from(file_config); - } - // Make sure we got both the shared secret and the AWS secret if (!s3->valid()) { TSError("[%s] requires both shared and AWS secret configuration", PLUGIN_NAME); @@ -1207,6 +1336,12 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE } } + if (s3->watch_config()) { + s3->start_watch_config(); + } else { + s3->stop_watch_config(); + } + *ih = static_cast(s3); TSDebug(PLUGIN_NAME, "New rule: access_key=%s, virtual_host=%s, version=%d", s3->keyid(), s3->virt_host() ? "yes" : "no", s3->version()); diff --git a/src/traffic_layout/info.cc b/src/traffic_layout/info.cc index a54a1b10f49..7b911793da8 100644 --- a/src/traffic_layout/info.cc +++ b/src/traffic_layout/info.cc @@ -108,6 +108,7 @@ produce_features(bool json) print_feature("TS_USE_EPOLL", TS_USE_EPOLL, json); print_feature("TS_USE_KQUEUE", TS_USE_KQUEUE, json); print_feature("TS_USE_PORT", TS_USE_PORT, json); + print_feature("TS_USE_INOTIFY", TS_USE_INOTIFY, json); print_feature("TS_USE_POSIX_CAP", TS_USE_POSIX_CAP, json); print_feature("TS_USE_TPROXY", TS_USE_TPROXY, json); print_feature("TS_HAS_SO_MARK", TS_HAS_SO_MARK, json); diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc index f470bbb8d19..abf8c94ee19 100644 --- a/src/traffic_server/InkAPI.cc +++ b/src/traffic_server/InkAPI.cc @@ -25,6 +25,10 @@ #include #include +#if defined(__linux__) +#include +#endif + #include "tscore/ink_platform.h" #include "tscore/ink_base64.h" #include "tscore/PluginUserArgs.h" @@ -74,6 +78,7 @@ #include "I_Machine.h" #include "HttpProxyServerMain.h" #include "shared/overridable_txn_vars.h" +#include "FileChange.h" #include "ts/ts.h" @@ -10465,3 +10470,19 @@ TSDbgCtlCreate(char const *tag) return DbgCtl::_get_ptr(tag); } + +tsapi TSWatchDescriptor +TSFileEventRegister(const char *path, TSFileWatchKind kind, TSCont contp) +{ + sdk_assert(sdk_sanity_check_iocore_structure(contp) == TS_SUCCESS); + sdk_assert(sdk_sanity_check_null_ptr((void *)this_ethread()) == TS_SUCCESS); + + Continuation *pCont = reinterpret_cast(contp); + return fileChangeManager.add(ts::file::path{path}, kind, pCont); +} + +tsapi void +TSFileEventUnregister(TSWatchDescriptor wd) +{ + fileChangeManager.remove(wd); +} diff --git a/src/traffic_server/Makefile.inc b/src/traffic_server/Makefile.inc index b908fea376a..c9f002ef3b9 100644 --- a/src/traffic_server/Makefile.inc +++ b/src/traffic_server/Makefile.inc @@ -82,6 +82,7 @@ traffic_server_traffic_server_LDADD = \ $(top_builddir)/iocore/net/libinknet.a \ $(top_builddir)/lib/records/librecords_p.a \ $(top_builddir)/iocore/eventsystem/libinkevent.a \ + $(top_builddir)/iocore/fs/libinkfs.a \ @HWLOC_LIBS@ \ @LIBPCRE@ \ @LIBRESOLV@ \ diff --git a/src/traffic_server/traffic_server.cc b/src/traffic_server/traffic_server.cc index 3a1a868a5dd..1214f550b85 100644 --- a/src/traffic_server/traffic_server.cc +++ b/src/traffic_server/traffic_server.cc @@ -103,6 +103,7 @@ extern "C" int plock(int); #include "HTTP2.h" #include "tscore/ink_config.h" #include "P_SSLClientUtils.h" +#include "FileChange.h" #if TS_USE_QUIC == 1 #include "Http3.h" @@ -2001,6 +2002,9 @@ main(int /* argc ATS_UNUSED */, const char **argv) proxyServerCheck.notify_one(); } + // Spawn a thread to do file system change notification + fileChangeManager.init(); + // !! ET_NET threads start here !! // This means any spawn scheduling must be done before this point. eventProcessor.start(num_of_net_threads, stacksize); diff --git a/tests/README.md b/tests/README.md index e28a6b90751..a97ba0221c3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -321,6 +321,7 @@ ts.Disk.remap_config.AddLine( * TS_HAS_128BIT_CAS * TS_HAS_TESTS * TS_HAS_WCCP + * TS_USE_INOTIFY ### Example ```python diff --git a/tests/gold_tests/pluginTest/s3_auth/gold/s3_auth_basic.gold b/tests/gold_tests/pluginTest/s3_auth/gold/s3_auth_basic.gold new file mode 100644 index 00000000000..5ec9ba4d49e --- /dev/null +++ b/tests/gold_tests/pluginTest/s3_auth/gold/s3_auth_basic.gold @@ -0,0 +1,34 @@ +* Trying 127.0.0.1:``... +* Connected to 127.0.0.1 (127.0.0.1) port `` (#0) +> GET / HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/`` +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< Date: `` +< Age: 0 +< Transfer-Encoding: chunked +< Connection: keep-alive +< Server: `` +< +{ [5 bytes data] +* Connection #0 to host 127.0.0.1 left intact +* Trying 127.0.0.1:``... +* Connected to 127.0.0.1 (127.0.0.1) port `` (#0) +> GET / HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/`` +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< Date: `` +< Age: 0 +< Transfer-Encoding: chunked +< Connection: keep-alive +< Server: `` +< +{ [5 bytes data] +* Connection #0 to host 127.0.0.1 left intact diff --git a/tests/gold_tests/pluginTest/s3_auth/gold/traffic_server.gold b/tests/gold_tests/pluginTest/s3_auth/gold/traffic_server.gold new file mode 100644 index 00000000000..4c41e8d8c9e --- /dev/null +++ b/tests/gold_tests/pluginTest/s3_auth/gold/traffic_server.gold @@ -0,0 +1,2 @@ +`` DIAG: (s3_auth) Updated rule: access_key=1111111, virtual_host=no, version=4 +`` diff --git a/tests/gold_tests/pluginTest/s3_auth/rules/region_map.conf b/tests/gold_tests/pluginTest/s3_auth/rules/region_map.conf new file mode 100644 index 00000000000..1f663336a8b --- /dev/null +++ b/tests/gold_tests/pluginTest/s3_auth/rules/region_map.conf @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +127.0.0.1 : us-east-1 diff --git a/tests/gold_tests/pluginTest/s3_auth/rules/v4-modified.conf b/tests/gold_tests/pluginTest/s3_auth/rules/v4-modified.conf new file mode 100644 index 00000000000..165d0b96309 --- /dev/null +++ b/tests/gold_tests/pluginTest/s3_auth/rules/v4-modified.conf @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +access_key=1111111 +secret_key=5555555 +version=4 +ttl=0 diff --git a/tests/gold_tests/pluginTest/s3_auth/rules/v4.conf b/tests/gold_tests/pluginTest/s3_auth/rules/v4.conf new file mode 100644 index 00000000000..822b5346187 --- /dev/null +++ b/tests/gold_tests/pluginTest/s3_auth/rules/v4.conf @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +access_key=1234567 +secret_key=9999999 +version=4 +ttl=0 diff --git a/tests/gold_tests/pluginTest/s3_auth/s3_auth_watch_config.test.py b/tests/gold_tests/pluginTest/s3_auth/s3_auth_watch_config.test.py new file mode 100644 index 00000000000..7453ad1e664 --- /dev/null +++ b/tests/gold_tests/pluginTest/s3_auth/s3_auth_watch_config.test.py @@ -0,0 +1,87 @@ +''' +Test s3_auth config change watch function +''' +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Test.ContinueOnFail = True + +Test.SkipUnless( + Condition.HasATSFeature('TS_USE_INOTIFY') or Condition.HasATSFeature('TS_USE_KQUEUE'), +) + +ts = Test.MakeATSProcess("ts") +server = Test.MakeOriginServer("server") + +Test.testName = "s3_auth: watch config" + +# define the request header and the desired response header +request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" +} + +# desired response form the origin server +response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" +} + +# add request/response +server.addResponse("sessionlog.log", request_header, response_header) + +ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'FileChange|s3_auth', +}) + +ts.Setup.CopyAs('rules/v4.conf', Test.RunDirectory) +ts.Setup.CopyAs('rules/v4-modified.conf', Test.RunDirectory) +ts.Setup.CopyAs('rules/region_map.conf', Test.RunDirectory) + +ts.Disk.remap_config.AddLine( + 'map http://www.example.com http://127.0.0.1:{0} \ + @plugin=s3_auth.so \ + @pparam=--config @pparam={1}/v4.conf \ + @pparam=--v4-region-map @pparam={1}/region_map.conf \ + @pparam=--watch-config \ + ' + .format(server.Variables.Port, Test.RunDirectory) +) + +# Commands to get the following response headers +# 1. make a request +# 2. modify the config +# 3. make another request +curlRequest = ( + 'curl -s -v -H "Host: www.example.com" http://127.0.0.1:{0};' + 'sleep 1; cp {1}/v4-modified.conf {1}/v4.conf;' + 'sleep 1; curl -s -v -H "Host: www.example.com" http://127.0.0.1:{0};' +) + +# Test Case +tr = Test.AddTestRun() +tr.Processes.Default.Command = curlRequest.format(ts.Variables.port, Test.RunDirectory) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.StartBefore(server) +tr.Processes.Default.StartBefore(ts) +tr.Processes.Default.Streams.stderr = "gold/s3_auth_basic.gold" +tr.StillRunningAfter = server + +ts.Disk.traffic_out.Content = "gold/traffic_server.gold" +ts.ReturnCode = 0